Passed
Push — master ( 47312b...0d4b48 )
by Andrey
01:13
created

cookiecutter.generate.generate_file()   B

Complexity

Conditions 7

Size

Total Lines 65
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 65
rs 7.8319
c 0
b 0
f 0
cc 7
nop 5

How to fix   Long Method   

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:

1
# -*- coding: utf-8 -*-
2
3
"""Functions for generating a project from a project template."""
4
from __future__ import unicode_literals
5
6
import fnmatch
7
import io
8
import json
9
import logging
10
import os
11
import shutil
12
from collections import OrderedDict
13
14
from binaryornot.check import is_binary
15
from jinja2 import FileSystemLoader
16
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
17
18
from cookiecutter.environment import StrictEnvironment
19
from cookiecutter.exceptions import (
20
    NonTemplatedInputDirException,
21
    ContextDecodingException,
22
    FailedHookException,
23
    OutputDirExistsException,
24
    UndefinedVariableInTemplate,
25
)
26
from cookiecutter.find import find_template
27
from cookiecutter.hooks import run_hook
28
from cookiecutter.utils import make_sure_path_exists, work_in, rmtree
29
30
logger = logging.getLogger(__name__)
31
32
33
def is_copy_only_path(path, context):
34
    """Check whether the given `path` should only be copied and not rendered.
35
36
    Returns True if `path` matches a pattern in the given `context` dict,
37
    otherwise False.
38
39
    :param path: A file-system path referring to a file or dir that
40
        should be rendered or just copied.
41
    :param context: cookiecutter context.
42
    """
43
    try:
44
        for dont_render in context['cookiecutter']['_copy_without_render']:
45
            if fnmatch.fnmatch(path, dont_render):
46
                return True
47
    except KeyError:
48
        return False
49
50
    return False
51
52
53
def apply_overwrites_to_context(context, overwrite_context):
54
    """Modify the given context in place based on the overwrite_context."""
55
    for variable, overwrite in overwrite_context.items():
56
        if variable not in context:
57
            # Do not include variables which are not used in the template
58
            continue
59
60
        context_value = context[variable]
61
62
        if isinstance(context_value, list):
63
            # We are dealing with a choice variable
64
            if overwrite in context_value:
65
                # This overwrite is actually valid for the given context
66
                # Let's set it as default (by definition first item in list)
67
                # see ``cookiecutter.prompt.prompt_choice_for_config``
68
                context_value.remove(overwrite)
69
                context_value.insert(0, overwrite)
70
        else:
71
            # Simply overwrite the value for this variable
72
            context[variable] = overwrite
73
74
75
def generate_context(
76
    context_file='cookiecutter.json', default_context=None, extra_context=None
77
):
78
    """Generate the context for a Cookiecutter project template.
79
80
    Loads the JSON file as a Python object, with key being the JSON filename.
81
82
    :param context_file: JSON file containing key/value pairs for populating
83
        the cookiecutter's variables.
84
    :param default_context: Dictionary containing config to take into account.
85
    :param extra_context: Dictionary containing configuration overrides
86
    """
87
    context = OrderedDict([])
88
89
    try:
90
        with open(context_file) as file_handle:
91
            obj = json.load(file_handle, object_pairs_hook=OrderedDict)
92
    except ValueError as e:
93
        # JSON decoding error.  Let's throw a new exception that is more
94
        # friendly for the developer or user.
95
        full_fpath = os.path.abspath(context_file)
96
        json_exc_message = str(e)
97
        our_exc_message = (
98
            'JSON decoding error while loading "{0}".  Decoding'
99
            ' error details: "{1}"'.format(full_fpath, json_exc_message)
100
        )
101
        raise ContextDecodingException(our_exc_message)
102
103
    # Add the Python object to the context dictionary
104
    file_name = os.path.split(context_file)[1]
105
    file_stem = file_name.split('.')[0]
106
    context[file_stem] = obj
107
108
    # Overwrite context variable defaults with the default context from the
109
    # user's global config, if available
110
    if default_context:
111
        apply_overwrites_to_context(obj, default_context)
112
    if extra_context:
113
        apply_overwrites_to_context(obj, extra_context)
114
115
    logger.debug('Context generated is %s', context)
116
    return context
117
118
119
def generate_file(project_dir, infile, context, env, skip_if_file_exists=False):
120
    """Render filename of infile as name of outfile, handle infile correctly.
121
122
    Dealing with infile appropriately:
123
124
        a. If infile is a binary file, copy it over without rendering.
125
        b. If infile is a text file, render its contents and write the
126
           rendered infile to outfile.
127
128
    Precondition:
129
130
        When calling `generate_file()`, the root template dir must be the
131
        current working directory. Using `utils.work_in()` is the recommended
132
        way to perform this directory change.
133
134
    :param project_dir: Absolute path to the resulting generated project.
135
    :param infile: Input file to generate the file from. Relative to the root
136
        template dir.
137
    :param context: Dict for populating the cookiecutter's variables.
138
    :param env: Jinja2 template execution environment.
139
    """
140
    logger.debug('Processing file %s', infile)
141
142
    # Render the path to the output file (not including the root project dir)
143
    outfile_tmpl = env.from_string(infile)
144
145
    outfile = os.path.join(project_dir, outfile_tmpl.render(**context))
146
    file_name_is_empty = os.path.isdir(outfile)
147
    if file_name_is_empty:
148
        logger.debug('The resulting file name is empty: %s', outfile)
149
        return
150
151
    if skip_if_file_exists and os.path.exists(outfile):
152
        logger.debug('The resulting file already exists: %s', outfile)
153
        return
154
155
    logger.debug('Created file at %s', outfile)
156
157
    # Just copy over binary files. Don't render.
158
    logger.debug("Check %s to see if it's a binary", infile)
159
    if is_binary(infile):
160
        logger.debug('Copying binary %s to %s without rendering', infile, outfile)
161
        shutil.copyfile(infile, outfile)
162
    else:
163
        # Force fwd slashes on Windows for get_template
164
        # This is a by-design Jinja issue
165
        infile_fwd_slashes = infile.replace(os.path.sep, '/')
166
167
        # Render the file
168
        try:
169
            tmpl = env.get_template(infile_fwd_slashes)
170
        except TemplateSyntaxError as exception:
171
            # Disable translated so that printed exception contains verbose
172
            # information about syntax error location
173
            exception.translated = False
174
            raise
175
        rendered_file = tmpl.render(**context)
176
177
        logger.debug('Writing contents to file %s', outfile)
178
179
        with io.open(outfile, 'w', encoding='utf-8') as fh:
180
            fh.write(rendered_file)
181
182
    # Apply file permissions to output file
183
    shutil.copymode(infile, outfile)
184
185
186
def render_and_create_dir(
187
    dirname, context, output_dir, environment, overwrite_if_exists=False
188
):
189
    """Render name of a directory, create the directory, return its path."""
190
    name_tmpl = environment.from_string(dirname)
191
    rendered_dirname = name_tmpl.render(**context)
192
193
    dir_to_create = os.path.normpath(os.path.join(output_dir, rendered_dirname))
194
195
    logger.debug(
196
        'Rendered dir %s must exist in output_dir %s', dir_to_create, output_dir
197
    )
198
199
    output_dir_exists = os.path.exists(dir_to_create)
200
201
    if output_dir_exists:
202
        if overwrite_if_exists:
203
            logger.debug(
204
                'Output directory %s already exists, overwriting it', dir_to_create
205
            )
206
        else:
207
            msg = 'Error: "{}" directory already exists'.format(dir_to_create)
208
            raise OutputDirExistsException(msg)
209
    else:
210
        make_sure_path_exists(dir_to_create)
211
212
    return dir_to_create, not output_dir_exists
213
214
215
def ensure_dir_is_templated(dirname):
216
    """Ensure that dirname is a templated directory name."""
217
    if '{{' in dirname and '}}' in dirname:
218
        return True
219
    else:
220
        raise NonTemplatedInputDirException
221
222
223
def _run_hook_from_repo_dir(
224
    repo_dir, hook_name, project_dir, context, delete_project_on_failure
225
):
226
    """Run hook from repo directory, clean project directory if hook fails.
227
228
    :param repo_dir: Project template input directory.
229
    :param hook_name: The hook to execute.
230
    :param project_dir: The directory to execute the script from.
231
    :param context: Cookiecutter project context.
232
    :param delete_project_on_failure: Delete the project directory on hook
233
        failure?
234
    """
235
    with work_in(repo_dir):
236
        try:
237
            run_hook(hook_name, project_dir, context)
238
        except FailedHookException:
239
            if delete_project_on_failure:
240
                rmtree(project_dir)
241
            logger.error(
242
                "Stopping generation because %s hook "
243
                "script didn't exit successfully",
244
                hook_name,
245
            )
246
            raise
247
248
249
def generate_files(
250
    repo_dir,
251
    context=None,
252
    output_dir='.',
253
    overwrite_if_exists=False,
254
    skip_if_file_exists=False,
255
):
256
    """Render the templates and saves them to files.
257
258
    :param repo_dir: Project template input directory.
259
    :param context: Dict for populating the template's variables.
260
    :param output_dir: Where to output the generated project dir into.
261
    :param overwrite_if_exists: Overwrite the contents of the output directory
262
        if it exists.
263
    """
264
    template_dir = find_template(repo_dir)
265
    logger.debug('Generating project from %s...', template_dir)
266
    context = context or OrderedDict([])
267
268
    unrendered_dir = os.path.split(template_dir)[1]
269
    ensure_dir_is_templated(unrendered_dir)
270
    env = StrictEnvironment(context=context, keep_trailing_newline=True)
271
    try:
272
        project_dir, output_directory_created = render_and_create_dir(
273
            unrendered_dir, context, output_dir, env, overwrite_if_exists
274
        )
275
    except UndefinedError as err:
276
        msg = "Unable to create project directory '{}'".format(unrendered_dir)
277
        raise UndefinedVariableInTemplate(msg, err, context)
278
279
    # We want the Jinja path and the OS paths to match. Consequently, we'll:
280
    #   + CD to the template folder
281
    #   + Set Jinja's path to '.'
282
    #
283
    #  In order to build our files to the correct folder(s), we'll use an
284
    # absolute path for the target folder (project_dir)
285
286
    project_dir = os.path.abspath(project_dir)
287
    logger.debug('Project directory is %s', project_dir)
288
289
    # if we created the output directory, then it's ok to remove it
290
    # if rendering fails
291
    delete_project_on_failure = output_directory_created
292
293
    _run_hook_from_repo_dir(
294
        repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure
295
    )
296
297
    with work_in(template_dir):
298
        env.loader = FileSystemLoader('.')
299
300
        for root, dirs, files in os.walk('.'):
301
            # We must separate the two types of dirs into different lists.
302
            # The reason is that we don't want ``os.walk`` to go through the
303
            # unrendered directories, since they will just be copied.
304
            copy_dirs = []
305
            render_dirs = []
306
307
            for d in dirs:
308
                d_ = os.path.normpath(os.path.join(root, d))
309
                # We check the full path, because that's how it can be
310
                # specified in the ``_copy_without_render`` setting, but
311
                # we store just the dir name
312
                if is_copy_only_path(d_, context):
313
                    copy_dirs.append(d)
314
                else:
315
                    render_dirs.append(d)
316
317
            for copy_dir in copy_dirs:
318
                indir = os.path.normpath(os.path.join(root, copy_dir))
319
                outdir = os.path.normpath(os.path.join(project_dir, indir))
320
                logger.debug('Copying dir %s to %s without rendering', indir, outdir)
321
                shutil.copytree(indir, outdir)
322
323
            # We mutate ``dirs``, because we only want to go through these dirs
324
            # recursively
325
            dirs[:] = render_dirs
326
            for d in dirs:
327
                unrendered_dir = os.path.join(project_dir, root, d)
328
                try:
329
                    render_and_create_dir(
330
                        unrendered_dir, context, output_dir, env, overwrite_if_exists
331
                    )
332
                except UndefinedError as err:
333
                    if delete_project_on_failure:
334
                        rmtree(project_dir)
335
                    _dir = os.path.relpath(unrendered_dir, output_dir)
336
                    msg = "Unable to create directory '{}'".format(_dir)
337
                    raise UndefinedVariableInTemplate(msg, err, context)
338
339
            for f in files:
340
                infile = os.path.normpath(os.path.join(root, f))
341
                if is_copy_only_path(infile, context):
342
                    outfile_tmpl = env.from_string(infile)
343
                    outfile_rendered = outfile_tmpl.render(**context)
344
                    outfile = os.path.join(project_dir, outfile_rendered)
345
                    logger.debug(
346
                        'Copying file %s to %s without rendering', infile, outfile
347
                    )
348
                    shutil.copyfile(infile, outfile)
349
                    shutil.copymode(infile, outfile)
350
                    continue
351
                try:
352
                    generate_file(
353
                        project_dir, infile, context, env, skip_if_file_exists
354
                    )
355
                except UndefinedError as err:
356
                    if delete_project_on_failure:
357
                        rmtree(project_dir)
358
                    msg = "Unable to create file '{}'".format(infile)
359
                    raise UndefinedVariableInTemplate(msg, err, context)
360
361
    _run_hook_from_repo_dir(
362
        repo_dir, 'post_gen_project', project_dir, context, delete_project_on_failure
363
    )
364
365
    return project_dir
366