Completed
Push — publish ( 6057b5...9b23c3 )
by Michael
17:30 queued 08:11
created

Config.__init__()   A

Complexity

Conditions 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 14
rs 9.4285
1
import textwrap
2
from collections import OrderedDict
3
from os.path import exists, expanduser, expandvars, join, curdir
4
import io
5
import os
6
import sys
7
8
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
from pathlib import Path
10
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
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
14
import changes
15
from changes.models import GitRepository
0 ignored issues
show
Bug introduced by
The name models does not seem to exist in module changes.
Loading history...
Configuration introduced by
Unable to import 'changes.models' (unexpected unindent (<string>, line 62))

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...
16
from .commands import info, note
17
18
19
AUTH_TOKEN_ENVVAR = 'GITHUB_AUTH_TOKEN'
20
21
# via https://github.com/jakubroztocil/httpie/blob/6bdfc7a/httpie/config.py#L9
22
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
PROJECT_CONFIG_FILE = '.changes.toml'
30
DEFAULT_RELEASES_DIRECTORY = 'docs/releases'
31
32
33
@attr.s
34
class Changes(object):
35
    auth_token = attr.ib()
36
37
    @classmethod
38
    def load(cls):
39
        tool_config_path = Path(str(os.environ.get(
40
            'CHANGES_CONFIG_FILE',
41
            expanduser('~/.changes') if not IS_WINDOWS else
42
            expandvars(r'%APPDATA%\\.changes')
43
        )))
44
45
        tool_settings = None
46
        if tool_config_path.exists():
47
            tool_settings = Changes(
48
                **(toml.load(tool_config_path.open())['changes'])
49
            )
50
51
        # envvar takes precedence over config file settings
52
        auth_token = os.environ.get(AUTH_TOKEN_ENVVAR)
53
        if auth_token:
54
            info('Found Github Auth Token in the environment')
55
            tool_settings = Changes(auth_token=auth_token)
56
        elif not (tool_settings and tool_settings.auth_token):
57
            while not auth_token:
58
                info('No auth token found, asking for it')
59
                # to interact with the Git*H*ub API
60
                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 (85/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 (86/79).

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

Loading history...
62
                            '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
66
            if not tool_settings:
67
                tool_settings = Changes(auth_token=auth_token)
68
69
            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
                toml.dumps({
71
                    'changes': attr.asdict(tool_settings)
72
                })
73
            )
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
    @classmethod
86
    def load(cls):
87
        repository = GitRepository(
88
            auth_token=changes.settings.auth_token
89
        )
90
91
        project_settings = configure_changes(repository)
92
        project_settings.repository = repository
93
        project_settings.bumpversion = BumpVersion.load(repository.latest_version)
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...
94
95
        return project_settings
96
97
98
@attr.s
99
class BumpVersion(object):
100
    DRAFT_OPTIONS = [
101
        '--dry-run', '--verbose',
102
        '--no-commit', '--no-tag',
103
        '--allow-dirty',
104
    ]
105
    STAGE_OPTIONS = [
106
        '--verbose', '--allow-dirty',
107
        '--no-commit', '--no-tag',
108
    ]
109
110
    current_version = attr.ib()
111
    version_files_to_replace = attr.ib(default=attr.Factory(list))
112
113
    @classmethod
114
    def load(cls, latest_version):
115
        return configure_bumpversion(latest_version)
116
117
    def write_to_file(self, config_path: Path):
118
        bumpversion_cfg = textwrap.dedent(
119
            """\
120
            [bumpversion]
121
            current_version = {current_version}
122
123
            """
124
        ).format(**attr.asdict(self))
125
126
        bumpversion_files = '\n\n'.join([
127
            '[bumpversion:file:{}]'.format(file_name)
128
            for file_name in self.version_files_to_replace
129
        ])
130
131
        config_path.write_text(
132
            bumpversion_cfg + bumpversion_files
133
        )
134
135
136
def configure_changes(repository):
137
    changes_project_config_path = Path(PROJECT_CONFIG_FILE)
138
    project_settings = None
139
    if changes_project_config_path.exists():
140
        # releases_directory, labels
141
        project_settings = Project(
142
            **(toml.load(changes_project_config_path.open())['changes'])
143
        )
144
145
    if not project_settings:
146
        releases_directory = Path(click.prompt(
147
            'Enter the directory to store your releases notes',
148
            DEFAULT_RELEASES_DIRECTORY,
149
            type=click.Path(exists=True, dir_okay=True)
150
        ))
151
152
        # releases_directory = Path(changes.project_settings.releases_directory)
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...
153
        if not releases_directory.exists():
154
            debug('Releases directory {} not found, creating it.'.format(releases_directory))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (93/79).

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

Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'debug'
Loading history...
155
            releases_directory.mkdir(parents=True)
156
157
        # FIXME: GitHub(repository).labels()
158
        project_settings = Project(
159
            releases_directory=releases_directory,
160
            labels=configure_labels(repository.github_labels()),
161
        )
162
        # write config file
163
        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...
164
            toml.dumps({
165
                'changes': attr.asdict(project_settings)
166
            })
167
        )
168
169
    return project_settings
170
171
172
def configure_bumpversion(latest_version):
173
    # TODO: look in other supported bumpversion config locations
174
    bumpversion = None
175
    bumpversion_config_path = Path('.bumpversion.cfg')
176
    if not bumpversion_config_path.exists():
177
        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...
178
179
        version_file_path_answer = None
180
        input_terminator = '.'
181
        while not version_file_path_answer == input_terminator:
182
            version_file_path_answer = click.prompt(
183
                'Enter a path to a file that contains a version number '
184
                "(enter a path of '.' when you're done selecting files)",
185
                type=click.Path(
186
                    exists=True,
187
                    dir_okay=True,
188
                    file_okay=True,
189
                    readable=True
190
                )
191
            )
192
193
            if version_file_path_answer != input_terminator:
194
                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 (83/79).

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

Loading history...
195
196
        bumpversion = BumpVersion(
197
            current_version=latest_version,
198
            version_files_to_replace=user_supplied_versioned_file_paths,
199
        )
200
        bumpversion.write_to_file(bumpversion_config_path)
201
202
    return bumpversion
203
204
205
def configure_labels(github_labels):
206
    # ask which github tags they want to track
207
    # TODO: streamlined support for github defaults: enhancement, bug
208
    # TODO: apply description transform in labels_prompt function
209
    changelog_worthy_labels = read_user_choices(
210
        'labels',
211
        [
212
            properties['name']
213
            for label, properties in github_labels.items()
214
        ]
215
    )
216
217
    described_labels = {}
218
    # auto-generate label descriptions
219
    for label_name in changelog_worthy_labels:
220
        label_properties = github_labels[label_name]
221
        # Auto-generate description as titlecase label name
222
        label_properties['description'] = label_name.title()
223
        described_labels[label_name] = label_properties
224
225
    return described_labels
226
227
228
def read_user_choices(var_name, options):
229
    """Prompt the user to choose from several options for the given variable.
230
231
    # cookiecutter/cookiecutter/prompt.py
232
    The first item will be returned if no input happens.
233
234
    :param str var_name: Variable as specified in the context
235
    :param list options: Sequence of options that are available to select from
236
    :return: Exactly one item of ``options`` that has been chosen by the user
237
    """
238
    raise NotImplementedError()
239
    #
240
241
    # Please see http://click.pocoo.org/4/api/#click.prompt
242
    if not isinstance(options, list):
243
        raise TypeError
244
245
    if not options:
246
        raise ValueError
247
248
    choice_map = OrderedDict(
249
        (u'{}'.format(i), value) for i, value in enumerate(options, 1)
250
    )
251
    choices = choice_map.keys()
252
    default = u'1'
253
254
    choice_lines = [u'{} - {}'.format(*c) for c in choice_map.items()]
255
    prompt = u'\n'.join((
256
        u'Select {}:'.format(var_name),
257
        u'\n'.join(choice_lines),
258
        u'Choose from {}'.format(u', '.join(choices))
259
    ))
260
261
    # TODO: multi-select
262
    user_choice = click.prompt(
263
        prompt, type=click.Choice(choices), default=default
264
    )
265
    return choice_map[user_choice]
266
267
DEFAULTS = {
268
    'changelog': 'CHANGELOG.md',
269
    'readme': 'README.md',
270
    'github_auth_token': None,
271
}
272
273
274
class Config:
275
    test_command = None
276
    pypi = None
277
    skip_changelog = None
278
    changelog_content = None
279
    repo = None
280
281
    def __init__(self, module_name, dry_run, debug, no_input, requirements,
282
                 new_version, current_version, repo_url, version_prefix):
283
        self.module_name = module_name
284
        # module_name => project_name => curdir
285
        self.dry_run = dry_run
286
        self.debug = debug
287
        self.no_input = no_input
288
        self.requirements = requirements
289
        self.new_version = (
290
            version_prefix + new_version
291
            if version_prefix
292
            else new_version
293
        )
294
        self.current_version = current_version
295
296
297
def project_config():
298
    """Deprecated"""
299
    project_name = curdir
300
301
    config_path = Path(join(project_name, PROJECT_CONFIG_FILE))
302
303
    if not exists(config_path):
304
        store_settings(DEFAULTS.copy())
305
        return DEFAULTS
306
307
    return toml.load(io.open(config_path)) or {}
308
309
310
def store_settings(settings):
311
    pass
312
313