Completed
Push — master ( 14c8ac...ce45ac )
by Michael
7s
created

generate_files()   F

Complexity

Conditions 12

Size

Total Lines 118

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 12
dl 0
loc 118
rs 2

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like generate_files() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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