Completed
Pull Request — master (#885)
by
unknown
01:08
created

generate_file()   C

Complexity

Conditions 7

Size

Total Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
dl 0
loc 69
rs 5.7452
c 0
b 0
f 0

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