cookiecutter.cli.main()   D
last analyzed

Complexity

Conditions 9

Size

Total Lines 167
Code Lines 129

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 129
dl 0
loc 167
rs 4.6666
c 0
b 0
f 0
cc 9
nop 17

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
"""Main `cookiecutter` CLI."""
2
import collections
3
import json
4
import os
5
import sys
6
7
import click
8
9
from cookiecutter import __version__
10
from cookiecutter.exceptions import (
11
    ContextDecodingException,
12
    FailedHookException,
13
    InvalidModeException,
14
    InvalidZipRepository,
15
    OutputDirExistsException,
16
    RepositoryCloneFailed,
17
    RepositoryNotFound,
18
    UndefinedVariableInTemplate,
19
    UnknownExtension,
20
)
21
from cookiecutter.log import configure_logger
22
from cookiecutter.main import cookiecutter
23
from cookiecutter.config import get_user_config
24
25
26
def version_msg():
27
    """Return the Cookiecutter version, location and Python powering it."""
28
    python_version = sys.version
29
    location = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30
    return f"Cookiecutter {__version__} from {location} (Python {python_version})"
31
32
33
def validate_extra_context(ctx, param, value):
34
    """Validate extra context."""
35
    for string in value:
36
        if '=' not in string:
37
            raise click.BadParameter(
38
                f"EXTRA_CONTEXT should contain items of the form key=value; "
39
                f"'{string}' doesn't match that form"
40
            )
41
42
    # Convert tuple -- e.g.: ('program_name=foobar', 'startsecs=66')
43
    # to dict -- e.g.: {'program_name': 'foobar', 'startsecs': '66'}
44
    return collections.OrderedDict(s.split('=', 1) for s in value) or None
45
46
47
def list_installed_templates(default_config, passed_config_file):
48
    """List installed (locally cloned) templates. Use cookiecutter --list-installed."""
49
    config = get_user_config(passed_config_file, default_config)
50
    cookiecutter_folder = config.get('cookiecutters_dir')
51
    if not os.path.exists(cookiecutter_folder):
52
        click.echo(
53
            f"Error: Cannot list installed templates. "
54
            f"Folder does not exist: {cookiecutter_folder}"
55
        )
56
        sys.exit(-1)
57
58
    template_names = [
59
        folder
60
        for folder in os.listdir(cookiecutter_folder)
61
        if os.path.exists(
62
            os.path.join(cookiecutter_folder, folder, 'cookiecutter.json')
63
        )
64
    ]
65
    click.echo(f'{len(template_names)} installed templates: ')
66
    for name in template_names:
67
        click.echo(f' * {name}')
68
69
70
@click.command(context_settings=dict(help_option_names=['-h', '--help']))
71
@click.version_option(__version__, '-V', '--version', message=version_msg())
72
@click.argument('template', required=False)
73
@click.argument('extra_context', nargs=-1, callback=validate_extra_context)
74
@click.option(
75
    '--no-input',
76
    is_flag=True,
77
    help='Do not prompt for parameters and only use cookiecutter.json file content. '
78
    'Defaults to deleting any cached resources and redownloading them. '
79
    'Cannot be combined with the --replay flag.',
80
)
81
@click.option(
82
    '-c',
83
    '--checkout',
84
    help='branch, tag or commit to checkout after git clone',
85
)
86
@click.option(
87
    '--directory',
88
    help='Directory within repo that holds cookiecutter.json file '
89
    'for advanced repositories with multi templates in it',
90
)
91
@click.option(
92
    '-v', '--verbose', is_flag=True, help='Print debug information', default=False
93
)
94
@click.option(
95
    '--replay',
96
    is_flag=True,
97
    help='Do not prompt for parameters and only use information entered previously. '
98
    'Cannot be combined with the --no-input flag or with extra configuration passed.',
99
)
100
@click.option(
101
    '--replay-file',
102
    type=click.Path(),
103
    default=None,
104
    help='Use this file for replay instead of the default.',
105
)
106
@click.option(
107
    '-f',
108
    '--overwrite-if-exists',
109
    is_flag=True,
110
    help='Overwrite the contents of the output directory if it already exists',
111
)
112
@click.option(
113
    '-s',
114
    '--skip-if-file-exists',
115
    is_flag=True,
116
    help='Skip the files in the corresponding directories if they already exist',
117
    default=False,
118
)
119
@click.option(
120
    '-o',
121
    '--output-dir',
122
    default='.',
123
    type=click.Path(),
124
    help='Where to output the generated project dir into',
125
)
126
@click.option(
127
    '--config-file', type=click.Path(), default=None, help='User configuration file'
128
)
129
@click.option(
130
    '--default-config',
131
    is_flag=True,
132
    help='Do not load a config file. Use the defaults instead',
133
)
134
@click.option(
135
    '--debug-file',
136
    type=click.Path(),
137
    default=None,
138
    help='File to be used as a stream for DEBUG logging',
139
)
140
@click.option(
141
    '--accept-hooks',
142
    type=click.Choice(['yes', 'ask', 'no']),
143
    default='yes',
144
    help='Accept pre/post hooks',
145
)
146
@click.option(
147
    '-l', '--list-installed', is_flag=True, help='List currently installed templates.'
148
)
149
@click.option(
150
    '--keep-project-on-failure',
151
    is_flag=True,
152
    help='Do not delete project folder on failure',
153
)
154
def main(
155
    template,
156
    extra_context,
157
    no_input,
158
    checkout,
159
    verbose,
160
    replay,
161
    overwrite_if_exists,
162
    output_dir,
163
    config_file,
164
    default_config,
165
    debug_file,
166
    directory,
167
    skip_if_file_exists,
168
    accept_hooks,
169
    replay_file,
170
    list_installed,
171
    keep_project_on_failure,
172
):
173
    """Create a project from a Cookiecutter project template (TEMPLATE).
174
175
    Cookiecutter is free and open source software, developed and managed by
176
    volunteers. If you would like to help out or fund the project, please get
177
    in touch at https://github.com/cookiecutter/cookiecutter.
178
    """
179
    # Commands that should work without arguments
180
    if list_installed:
181
        list_installed_templates(default_config, config_file)
182
        sys.exit(0)
183
184
    # Raising usage, after all commands that should work without args.
185
    if not template or template.lower() == 'help':
186
        click.echo(click.get_current_context().get_help())
187
        sys.exit(0)
188
189
    configure_logger(stream_level='DEBUG' if verbose else 'INFO', debug_file=debug_file)
190
191
    # If needed, prompt the user to ask whether or not they want to execute
192
    # the pre/post hooks.
193
    if accept_hooks == "ask":
194
        _accept_hooks = click.confirm("Do you want to execute hooks?")
195
    else:
196
        _accept_hooks = accept_hooks == "yes"
197
198
    if replay_file:
199
        replay = replay_file
200
201
    try:
202
        cookiecutter(
203
            template,
204
            checkout,
205
            no_input,
206
            extra_context=extra_context,
207
            replay=replay,
208
            overwrite_if_exists=overwrite_if_exists,
209
            output_dir=output_dir,
210
            config_file=config_file,
211
            default_config=default_config,
212
            password=os.environ.get('COOKIECUTTER_REPO_PASSWORD'),
213
            directory=directory,
214
            skip_if_file_exists=skip_if_file_exists,
215
            accept_hooks=_accept_hooks,
216
            keep_project_on_failure=keep_project_on_failure,
217
        )
218
    except (
219
        ContextDecodingException,
220
        OutputDirExistsException,
221
        InvalidModeException,
222
        FailedHookException,
223
        UnknownExtension,
224
        InvalidZipRepository,
225
        RepositoryNotFound,
226
        RepositoryCloneFailed,
227
    ) as e:
228
        click.echo(e)
229
        sys.exit(1)
230
    except UndefinedVariableInTemplate as undefined_err:
231
        click.echo(f'{undefined_err.message}')
232
        click.echo(f'Error message: {undefined_err.error.message}')
233
234
        context_str = json.dumps(undefined_err.context, indent=4, sort_keys=True)
235
        click.echo(f'Context: {context_str}')
236
        sys.exit(1)
237
238
239
if __name__ == "__main__":
240
    main()
241