Completed
Push — pyup-update-toml-0.9.2-to-0.9.... ( 3e288d )
by Michael
21:07 queued 21:02
created

load_project_settings()   A

Complexity

Conditions 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
c 3
b 0
f 0
dl 0
loc 13
ccs 0
cts 0
cp 0
crap 2
rs 9.4285
1 3
import re
2 3
import textwrap
3 3
from collections import OrderedDict
4 3
from configparser import RawConfigParser
5 3
from os.path import exists, expanduser, expandvars, join, curdir
6 3
import io
7 3
import os
8 3
import sys
9
10 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...
11 3
from pathlib import Path
12
13 3
import inflection
0 ignored issues
show
Configuration introduced by
The import inflection 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...
14 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...
15 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...
16
17 3
import changes
18 3
from changes.models import GitRepository
19 3
from .commands import info, note, debug, error
20
21 3
AUTH_TOKEN_ENVVAR = 'GITHUB_AUTH_TOKEN'
22
23
# via https://github.com/jakubroztocil/httpie/blob/6bdfc7a/httpie/config.py#L9
24 3
IS_WINDOWS = 'win32' in str(sys.platform).lower()
25 3
DEFAULT_CONFIG_FILE = str(os.environ.get(
26
    'CHANGES_CONFIG_FILE',
27
    expanduser('~/.changes') if not IS_WINDOWS else
28
    expandvars(r'%APPDATA%\\.changes')
29
))
30
31 3
PROJECT_CONFIG_FILE = '.changes.toml'
32 3
DEFAULT_RELEASES_DIRECTORY = 'docs/releases'
33
34
35 3
@attr.s
36 3
class Changes(object):
37 3
    auth_token = attr.ib()
38
39 3
    @classmethod
40
    def load(cls):
41 3
        tool_config_path = Path(str(os.environ.get(
42
            'CHANGES_CONFIG_FILE',
43
            expanduser('~/.changes') if not IS_WINDOWS else
44
            expandvars(r'%APPDATA%\\.changes')
45
        )))
46
47 3
        tool_settings = None
48 3
        if tool_config_path.exists():
49 3
            tool_settings = Changes(
50
                **(toml.load(tool_config_path.open())['changes'])
51
            )
52
53
        # envvar takes precedence over config file settings
54 3
        auth_token = os.environ.get(AUTH_TOKEN_ENVVAR)
55 3
        if auth_token:
56 3
            info('Found Github Auth Token in the environment')
57 3
            tool_settings = Changes(auth_token=auth_token)
58 3
        elif not (tool_settings and tool_settings.auth_token):
59 3
            while not auth_token:
60 3
                info('No auth token found, asking for it')
61
                # to interact with the Git*H*ub API
62 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 (85/79).

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

Loading history...
63 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 (86/79).

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

Loading history...
64
                            'token" page, to create a token for changes.')
65 3
                click.launch('https://github.com/settings/tokens/new')
66 3
                auth_token = click.prompt('Enter your changes token')
67
68 3
            if not tool_settings:
69 3
                tool_settings = Changes(auth_token=auth_token)
70
71 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...
72
                toml.dumps({
73
                    'changes': attr.asdict(tool_settings)
74
                })
75
            )
76
77 3
        return tool_settings
78
79
80 3
@attr.s
81 3
class Project(object):
82 3
    releases_directory = attr.ib()
83 3
    repository = attr.ib(default=None)
84 3
    bumpversion = attr.ib(default=None)
85 3
    labels = attr.ib(default=attr.Factory(dict))
86
87 3
    @classmethod
88
    def load(cls):
89 3
        repository = GitRepository(
90
            auth_token=changes.settings.auth_token
91
        )
92
93 3
        project_settings = configure_changes(repository)
94 3
        project_settings.repository = repository
95 3
        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...
96
97 3
        return project_settings
98
99
100 3
@attr.s
101 3
class BumpVersion(object):
102 3
    DRAFT_OPTIONS = [
103
        '--dry-run', '--verbose',
104
        '--no-commit', '--no-tag',
105
        '--allow-dirty',
106
    ]
107 3
    STAGE_OPTIONS = [
108
        '--verbose', '--allow-dirty',
109
        '--no-commit', '--no-tag',
110
    ]
111
112 3
    current_version = attr.ib()
113 3
    version_files_to_replace = attr.ib(default=attr.Factory(list))
114
115 3
    @classmethod
116
    def load(cls, latest_version):
117 3
        return configure_bumpversion(latest_version)
118
119 3
    @classmethod
120 3
    def read_from_file(cls, config_path: Path):
121 3
        config = RawConfigParser('')
122 3
        config.readfp(config_path.open('rt', encoding='utf-8'))
123
124 3
        current_version = config.get("bumpversion", 'current_version')
125
126 3
        filenames = []
127 3
        for section_name in config.sections():
128
129 3
            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...
130
131 3
            if not section_name_match:
132 3
                continue
133
134 3
            section_prefix, section_value = section_name_match.groups()
135
136 3
            if section_prefix == "file":
137 3
                filenames.append(section_value)
138
139 3
        return cls(
140
            current_version=current_version,
141
            version_files_to_replace=filenames,
142
        )
143
144 3
    def write_to_file(self, config_path: Path):
145 3
        bumpversion_cfg = textwrap.dedent(
146
            """\
147
            [bumpversion]
148
            current_version = {current_version}
149
150
            """
151
        ).format(**attr.asdict(self))
152
153 3
        bumpversion_files = '\n\n'.join([
154
            '[bumpversion:file:{}]'.format(file_name)
155
            for file_name in self.version_files_to_replace
156
        ])
157
158 3
        config_path.write_text(
159
            bumpversion_cfg + bumpversion_files
160
        )
161
162
163 3
def configure_changes(repository):
164 3
    changes_project_config_path = Path(PROJECT_CONFIG_FILE)
165 3
    project_settings = None
166 3
    if changes_project_config_path.exists():
167
        # releases_directory, labels
168 3
        project_settings = Project(
169
            **(toml.load(changes_project_config_path.open())['changes'])
170
        )
171
172 3
    if not project_settings:
173 3
        releases_directory = Path(click.prompt(
174
            'Enter the directory to store your releases notes',
175
            DEFAULT_RELEASES_DIRECTORY,
176
            type=click.Path(exists=True, dir_okay=True)
177
        ))
178
179 3
        if not releases_directory.exists():
180 3
            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...
181 3
            releases_directory.mkdir(parents=True)
182
183
        # FIXME: GitHub(repository).labels()
184 3
        project_settings = Project(
185
            releases_directory=str(releases_directory),
186
            labels=configure_labels(repository.github_labels()),
187
        )
188
        # write config file
189 3
        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...
190
            toml.dumps({
191
                'changes': attr.asdict(project_settings)
192
            })
193
        )
194
195 3
    return project_settings
196
197
198 3
def configure_bumpversion(latest_version):
199
    # TODO: look in other supported bumpversion config locations
200 3
    bumpversion = None
201 3
    bumpversion_config_path = Path('.bumpversion.cfg')
202 3
    if not bumpversion_config_path.exists():
203 3
        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...
204
205 3
        version_file_path_answer = None
206 3
        input_terminator = '.'
207 3
        while not version_file_path_answer == input_terminator:
208 3
            version_file_path_answer = click.prompt(
209
                'Enter a path to a file that contains a version number '
210
                "(enter a path of '.' when you're done selecting files)",
211
                type=click.Path(
212
                    exists=True,
213
                    dir_okay=True,
214
                    file_okay=True,
215
                    readable=True
216
                )
217
            )
218
219 3
            if version_file_path_answer != input_terminator:
220 3
                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...
221
222 3
        bumpversion = BumpVersion(
223
            current_version=latest_version,
224
            version_files_to_replace=user_supplied_versioned_file_paths,
225
        )
226 3
        bumpversion.write_to_file(bumpversion_config_path)
227
228 3
    return bumpversion
229
230
231 3
def configure_labels(github_labels):
232 3
    labels_keyed_by_name = {}
233 3
    for label in github_labels:
234 3
        labels_keyed_by_name[label['name']] = label
235
236
    # TODO: streamlined support for github defaults: enhancement, bug
237 3
    changelog_worthy_labels = choose_labels([
238
        properties['name']
239
        for _, properties in labels_keyed_by_name.items()
240
    ])
241
242
    # TODO: apply description transform in labels_prompt function
243 3
    described_labels = {}
244
    # auto-generate label descriptions
245 3
    for label_name in changelog_worthy_labels:
246 3
        label_properties = labels_keyed_by_name[label_name]
247
        # Auto-generate description as pluralised titlecase label name
248 3
        label_properties['description'] = inflection.pluralize(
249
            inflection.titleize(label_name)
250
        )
251
252 3
        described_labels[label_name] = label_properties
253
254 3
    return described_labels
255
256
257 3
def choose_labels(alternatives):
258
    """
259
    Prompt the user select several labels from the provided alternatives.
260
261
    At least one label must be selected.
262
263
    :param list alternatives: Sequence of options that are available to select from
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...
264
    :return: Several selected labels
265
    """
266
    if not alternatives:
267
        raise ValueError
268
269
    if not isinstance(alternatives, list):
270
        raise TypeError
271
272
    choice_map = OrderedDict(
273
      ('{}'.format(i), value) for i, value in enumerate(alternatives, 1)
274
    )
275
    # prepend a termination option
276
    input_terminator = '0'
277
    choice_map.update({input_terminator: '<done>'})
278
    choice_map.move_to_end('0', last=False)
279
280
    choice_indexes = choice_map.keys()
281
282
    choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()]
283
    prompt = '\n'.join((
284
        'Select labels:',
285
        '\n'.join(choice_lines),
286
        'Choose from {}'.format(', '.join(choice_indexes))
287
    ))
288
289
    user_choices = set()
290
    user_choice = None
291
292
    while not user_choice == input_terminator:
293
        if user_choices:
294
            note('Selected labels: [{}]'.format(', '.join(user_choices)))
295
296
        user_choice = click.prompt(
297
            prompt,
298
            type=click.Choice(choice_indexes),
299
            default=input_terminator,
300
        )
301
        done = user_choice == input_terminator
302
        new_selection = user_choice not in user_choices
303
        nothing_selected = not user_choices
304
305
        if not done and new_selection:
306
            user_choices.add(choice_map[user_choice])
307
308
        if done and nothing_selected:
309
            error('Please select at least one label')
310
            user_choice = None
311
312
    return user_choices
313
314 3
DEFAULTS = {
315
    'changelog': 'CHANGELOG.md',
316
    'readme': 'README.md',
317
    'github_auth_token': None,
318
}
319
320
321 3
class Config:
322 3
    test_command = None
323 3
    pypi = None
324 3
    skip_changelog = None
325 3
    changelog_content = None
326 3
    repo = None
327
328 3
    def __init__(self, module_name, dry_run, debug, no_input, requirements,
0 ignored issues
show
Comprehensibility Bug introduced by
debug is re-defining a name which is already available in the outer-scope (previously defined on line 19).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
329
                 new_version, current_version, repo_url, version_prefix):
330 3
        self.module_name = module_name
331
        # module_name => project_name => curdir
332 3
        self.dry_run = dry_run
333 3
        self.debug = debug
334 3
        self.no_input = no_input
335 3
        self.requirements = requirements
336 3
        self.new_version = (
337
            version_prefix + new_version
338
            if version_prefix
339
            else new_version
340
        )
341 3
        self.current_version = current_version
342
343
344 3
def project_config():
345
    """Deprecated"""
346
    project_name = curdir
347
348
    config_path = Path(join(project_name, PROJECT_CONFIG_FILE))
349
350
    if not exists(config_path):
351
        store_settings(DEFAULTS.copy())
352
        return DEFAULTS
353
354
    return toml.load(io.open(config_path)) or {}
355
356
357 3
def store_settings(settings):
358
    pass
359
360