8000 Strict jinja environment by hackebrot · Pull Request #598 · cookiecutter/cookiecutter · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Strict jinja environment #598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8000
20 changes: 18 additions & 2 deletions cookiecutter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
import os
import sys
import logging
import json

import click

from cookiecutter import __version__
from cookiecutter.config import USER_CONFIG_PATH
from cookiecutter.main import cookiecutter
from cookiecutter.exceptions import (
OutputDirExistsException, InvalidModeException, FailedHookException
OutputDirExistsException,
InvalidModeException,
FailedHookException,
UndefinedVariableInTemplate
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -100,9 +104,21 @@ def main(template, no_input, checkout, verbose, replay, overwrite_if_exists,
config_file=user_config
)
except (OutputDirExistsException,
InvalidModeException, FailedHookException) as e:
InvalidModeException,
FailedHookException) as e:
click.echo(e)
sys.exit(1)
except UndefinedVariableInTemplate as undefined_err:
click.echo('{}'.format(undefined_err.message))
click.echo('Error message: {}'.format(undefined_err.error.message))

context_str = json.dumps(
undefined_err.context,
indent=4,
sort_keys=True
)
click.echo('Context: {}'.format(context_str))
sys.exit(1)


if __name__ == "__main__":
Expand Down
17 changes: 17 additions & 0 deletions cookiecutter/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,20 @@ class FailedHookException(CookiecutterException):
"""
Raised when a hook script fails
"""


class UndefinedVariableInTemplate(CookiecutterException):
"""Raised when a template uses a variable which is not defined in the
context.
"""
def __init__(self, message, error, context):
self.message = message
self.error = error
self.context = context

def __str__(self):
return (
"{self.message}. "
"Error message: {self.error.message}. "
"Context: {self.context}"
).format(**locals())
57 changes: 42 additions & 15 deletions cookiecutter/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
import os
import shutil

from jinja2 import FileSystemLoader, Template
from jinja2 import FileSystemLoader, StrictUndefined
from jinja2.environment import Environment
from jinja2.exceptions import TemplateSyntaxError
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
from binaryornot.check import is_binary

from .exceptions import (
NonTemplatedInputDirException,
ContextDecodingException,
FailedHookException,
OutputDirExistsException
OutputDirExistsException,
UndefinedVariableInTemplate
)
from .find import find_template
from .utils import make_sure_path_exists, work_in, rmtree
Expand Down Expand Up @@ -141,7 +142,7 @@ def gene DD41 rate_file(project_dir, infile, context, env):
logging.debug('Generating file {0}'.format(infile))

# Render the path to the output file (not including the root project dir)
outfile_tmpl = Template(infile)
outfile_tmpl = env.from_string(infile)

outfile = os.path.join(project_dir, outfile_tmpl.render(**context))
file_name_is_empty = os.path.isdir(outfile)
Expand Down Expand Up @@ -181,14 +182,14 @@ def generate_file(project_dir, infile, context, env):
shutil.copymode(infile, outfile)


def render_and_create_dir(dirname, context, output_dir,
def render_and_create_dir(dirname, context, output_dir, environment,
overwrite_if_exists=False):
"""
Renders the name of a directory, creates the directory, and
returns its path.
"""

name_tmpl = Template(dirname)
name_tmpl = environment.from_string(dirname)
rendered_dirname = name_tmpl.render(**context)
logging.debug('Rendered dir {0} must exist in output_dir {1}'.format(
rendered_dirname,
Expand Down Expand Up @@ -255,10 +256,21 @@ def generate_files(repo_dir, context=None, output_dir='.',

unrendered_dir = os.path.split(template_dir)[1]
ensure_dir_is_templated(unrendered_dir)
project_dir = render_and_create_dir(unrendered_dir,
context,
output_dir,
overwrite_if_exists)
env = Environment(
keep_trailing_newline=True,
undefined=StrictUndefined
)
try:
project_dir = render_and_create_dir(
unrendered_dir,
context,
output_dir,
env,
overwrite_if_exists
)
except UndefinedError as err:
msg = "Unable to create project directory '{}'".format(unrendered_dir)
raise UndefinedVariableInTemplate(msg, err, context)

# We want the Jinja path and the OS paths to match. Consequently, we'll:
# + CD to the template folder
Expand All @@ -273,7 +285,6 @@ def generate_files(repo_dir, context=None, output_dir='.',
_run_hook_from_repo_dir(repo_dir, 'pre_gen_project', project_dir, context)

with work_in(template_dir):
env = Environment(keep_trailing_newline=True)
env.loader = FileSystemLoader('.')

for root, dirs, files in os.walk('.'):
Expand Down Expand Up @@ -307,13 +318,24 @@ def generate_files(repo_dir, context=None, output_dir='.',
dirs[:] = render_dirs
for d in dirs:
unrendered_dir = os.path.join(project_dir, root, d)
render_and_create_dir(unrendered_dir, context, output_dir,
overwrite_if_exists)
try:
render_and_create_dir(
unrendered_dir,
context,
output_dir,
env,
overwrite_if_exists
)
except UndefinedError as err:
rmtree(project_dir)
_dir = os.path.relpath(unrendered_dir, output_dir)
msg = "Unable to create directory '{}'".format(_dir)
raise UndefinedVariableInTemplate(msg, err, context)

for f in files:
infile = os.path.normpath(os.path.join(root, f))
if copy_without_render(infile, context):
outfile_tmpl = Template(infile)
outfile_tmpl = env.from_string(infile)
outfile_rendered = outfile_tmpl.render(**context)
outfile = os.path.join(project_dir, outfile_rendered)
logging.debug(
Expand All @@ -324,7 +346,12 @@ def generate_files(repo_dir, context=None, output_dir='.',
shutil.copymode(infile, outfile)
continue
logging.debug('f is {0}'.format(f))
generate_file(project_dir, infile, context, env)
try:
generate_file(project_dir, infile, context, env)
except UndefinedError as err:
rmtree(project_dir)
msg = "Unable to create file '{}'".format(infile)
raise UndefinedVariableInTemplate(msg, err, context)

_run_hook_from_repo_dir(repo_dir, 'post_gen_project', project_dir, context)

Expand Down
4 changes: 2 additions & 2 deletions tests/hooks-abort-render/{{cookiecutter.repo_dir}}/README.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
{{cookiecutter.repo_name}}
==========================
{{cookiecutter.repo_dir}}
=========================
Original file line number Diff line number Diff line change
@@ -1 +1 @@
I eat {{ binary_test }}
I eat {{ cookiecutter.binary_test }}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
I eat {{ binary_test }}
I eat {{ cookiecutter.binary_test }}
31 changes: 31 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import json
import pytest

from click.testing import CliRunner
Expand Down Expand Up @@ -295,3 +296,33 @@ def test_default_user_config(mocker):
output_dir='.',
config_file=None
)


def test_echo_undefined_variable_error(tmpdir):
output_dir = str(tmpdir.mkdir('output'))
template_path = 'tests/undefined-variable/file-name/'

result = runner.invoke(main, [
'--no-input',
'--default-config',
'--output-dir',
output_dir,
template_path,
])

assert result.exit_code == 1

error = "Unable to create file '{{cookiecutter.foobar}}'"
assert error in result.output

message = "Error message: 'dict object' has no attribute 'foobar'"
assert message in result.output

context = {
'cookiecutter': {
'github_username': 'hackebrot',
'project_slug': 'testproject'
}
}
context_str = json.dumps(context, indent=4, sort_keys=True)
assert context_str in result.output
5 changes: 4 additions & 1 deletion tests/test_cookiecutters.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ def bake_data():
)

yield pypackage_data
yield jquery_data

# TODO: Remove xfail as soon as PR has been accepted
# https://github.com/audreyr/cookiecutter-jquery/pull/2
yield pytest.mark.xfail(jquery_data, reason='Undefined variable')


@skipif_travis
Expand Down
21 changes: 21 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-

from jinja2.exceptions import UndefinedError

from cookiecutter import exceptions


def test_undefined_variable_to_str():
undefined_var_error = exceptions.UndefinedVariableInTemplate(
'Beautiful is better than ugly',
UndefinedError('Errors should never pass silently'),
{'cookiecutter': {'foo': 'bar'}}
)

expected_str = (
"Beautiful is better than ugly. "
"Error message: Errors should never pass silently. "
"Context: {'cookiecutter': {'foo': 'bar'}}"
)

assert str(undefined_var_error) == expected_str
79 changes: 79 additions & 0 deletions tests/test_generate_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,82 @@ def test_generate_files_permissions():
)
input_script_file_mode = os.stat(input_script_file).st_mode & 0o777
assert tests_script_file_mode == input_script_file_mode


@pytest.fixture
def undefined_context():
return {
'cookiecutter': {
'project_slug': 'testproject',
'github_username': 'hackebrot'
}
}


def test_raise_undefined_variable_file_name(tmpdir, undefined_context):
output_dir = tmpdir.mkdir('output')

with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
generate.generate_files(
repo_dir='tests/undefined-variable/file-name/',
output_dir=str(output_dir),
context=undefined_context
)
error = err.value
assert "Unable to create file '{{cookiecutter.foobar}}'" == error.message
assert error.context == undefined_context

assert not output_dir.join('testproject').exists()


def test_raise_undefined_variable_file_content(tmpdir, undefined_context):
output_dir = tmpdir.mkdir('output')

with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
generate.generate_files(
repo_dir='tests/undefined-variable/file-content/',
output_dir=str(output_dir),
context=undefined_context
)
error = err.value
assert "Unable to create file 'README.rst'" == error.message
assert error.context == undefined_context

assert not output_dir.join('testproject').exists()


def test_raise_undefined_variable_dir_name(tmpdir, undefined_context):
output_dir = tmpdir.mkdir('output')

with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
generate.generate_files(
repo_dir='tests/undefined-variable/dir-name/',
output_dir=str(output_dir),
context=undefined_context
)
error = err.value

directory = os.path.join('testproject', '{{cookiecutter.foobar}}')
msg = "Unable to create directory '{}'".format(directory)
assert msg == error.message

assert error.context == undefined_context

assert not output_dir.join('testproject').exists()


def test_raise_undefined_variable_project_dir(tmpdir):
output_dir = tmpdir.mkdir('output')

with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
generate.generate_files(
repo_dir='tests/undefined-variable/dir-name/',
output_dir=str(output_dir),
context={}
)
error = err.value
msg = "Unable to create project directory '{{cookiecutter.project_slug}}'"
assert msg == error.message
assert error.context == {}

assert not output_dir.join('testproject').exists()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{cookiecutter.project_slug}}
{% for _ in cookiecutter.project_slug %}={% endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{cookiecutter.project_slug}}
{% for _ in cookiecutter.project_slug %}={% endfor %}

{{cookiecutter.foobar}}

https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}
4 changes: 4 additions & 0 deletions tests/undefined-variable/file-name/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"project_slug": "testproject",
"github_username": "hackebrot"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{cookiecutter.project_slug}}
{% for _ in cookiecutter.project_slug %}={% endfor %}
0