8000 Fixed #479 - Added support for dictionary data in configurations. by freakboy3742 · Pull Request #815 · cookiecutter/cookiecutter · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fixed #479 - Added support for dictionary data in configurations. #815

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 2 commits into from
Oct 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 57 additions & 3 deletions cookiecutter/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

from collections import OrderedDict
import json

import click
from past.builtins import basestring
Expand Down Expand Up @@ -83,11 +84,43 @@ def read_user_choice(var_name, options):
return choice_map[user_choice]


def read_user_dict(var_name, default_value):
"""Prompt the user to provide a dictionary of data.

:param str var_name: Variable as specified in the context
:param default_value: Value that will be returned if no input is provided
:return: A Python dictionary to use in the context.
"""
# Please see http://click.pocoo.org/4/api/#click.prompt
if not isinstance(default_value, dict):
raise TypeError

raw = click.prompt(var_name, default='default')
if raw != 'default':
value = json.loads(raw, object_hook=OrderedDict)
else:
value = default_value

return value


def render_variable(env, raw, cookiecutter_dict):
if raw is None:
return None
if not isinstance(raw, basestring):
elif isinstance(raw, dict):
return {
render_variable(env, k, cookiecutter_dict):
render_variable(env, v, cookiecutter_dict)
for k, v in raw.items()
}
elif isinstance(raw, list):
return [
render_variable(env, v, cookiecutter_dict)
for v in raw
]
elif not isinstance(raw, basestring):
raw = str(raw)

template = env.from_string(raw)

rendered_template = template.render(cookiecutter=cookiecutter_dict)
Expand Down Expand Up @@ -117,6 +150,9 @@ def prompt_for_config(context, no_input=False):
cookiecutter_dict = {}
env = StrictEnvironment(context=context)

# First pass: Handle simple and raw variables, plus choices.
# These must be done first because the dictionaries keys and
# values might refer to them.
for key, raw in iteritems(context[u'cookiecutter']):
if key.startswith(u'_'):
cookiecutter_dict[key] = raw
Expand All @@ -128,15 +164,33 @@ def prompt_for_config(context, no_input=False):
val = prompt_choice_for_config(
cookiecutter_dict, env, key, raw, no_input
)
else:
cookiecutter_dict[key] = val
elif not isinstance(raw, dict):
# We are dealing with a regular variable
val = render_variable(env, raw, cookiecutter_dict)

if not no_input:
val = read_user_variable(key, val)

cookiecutter_dict[key] = val
except UndefinedError as err:
msg = "Unable to render variable '{}'".format(key)
raise UndefinedVariableInTemplate(msg, err, context)

# Second pass; handle the dictionaries.
for key, raw in iteritems(context[u'cookiecutter']):

try:
if isinstance(raw, dict):
# We are dealing with a dict variable
val = render_variable(env, raw, cookiecutter_dict)

if not no_input:
val = read_user_dict(key, val)

cookiecutter_dict[key] = val
except UndefinedError as err:
msg = "Unable to render variable '{}'".format(key)
raise UndefinedVariableInTemplate(msg, err, context)

cookiecutter_dict[key] = val
return cookiecutter_dict
62 changes: 62 additions & 0 deletions docs/advanced/dict_variables.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.. _dict-variables:

Dictionary Variables (1.5+)
---------------------------

Dictionary variables provide a way to define deep structured information when
rendering a template.

Basic Usage
~~~~~~~~~~~

Dictionary variables are, as the name suggests, dictionaries of key-value
pairs. The dictionary values can, themselves, be other dictionaries and lists
- the data structure can be as deep as you need.

For example, you could provide the following dictionary variable in your
``cookiecutter.json``::

{
"file_types": {
"png": {
"name": "Portable Network Graphic",
"library": "libpng"
"apps": [
"GIMP",
]
},
"bmp": {
"name": "Bitmap",
"library": "libbmp",
"apps": [
"Paint",
"GIMP",
]
}
}
}

The above ``file_type`` dictionary variable creates
``cookiecutter.file_types``, which can be used like this::

{% for extension, details in cookiecutter.file_types.items %}
<dl>
<dt>Format name:</dt>
<dd>{{ details.name }}</dd>

<dt>Extension:</dt>
<dd>{{ extension }}</dd>

<dt>Applications:</dt>
<dd>
<ul>
{% for app in details.apps %}
<li>{{ details.name }}</li>
</ul>
</dd>
</dl>

{% endfor %}

Cookiecutter is using `Jinja2's for expression <http://jinja.pocoo.org/docs/dev/templates/#for>`_ to iterate over the items in the dictionary.

1 change: 1 addition & 0 deletions docs/advanced/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Various advanced topics regarding cookiecutter usage.
replay
cli_options
choice_variables
dict_variables
template_extensions
171 changes: 171 additions & 0 deletions tests/test_prompt.py
F9B4
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,147 @@ def test_prompt_for_config_unicode(self, monkeypatch):
cookiecutter_dict = prompt.prompt_for_config(context)
assert cookiecutter_dict == {'full_name': u'Pizzä ïs Gööd'}

def test_prompt_for_config_empty_dict(self, monkeypatch):
monkeypatch.setattr(
'cookiecutter.prompt.read_user_dict',
lambda var, default: {}
)
context = {'cookiecutter': {'details': {}}}

cookiecutter_dict = prompt.prompt_for_config(context)
assert cookiecutter_dict == {'details': {}}

def test_prompt_for_config_dict(self, monkeypatch):
monkeypatch.setattr(
'cookiecutter.prompt.read_user_dict',
lambda var, default: {"key": "value", "integer": 37}
)
context = {'cookiecutter': {'details': {}}}

cookiecutter_dict = prompt.prompt_for_config(context)
assert cookiecutter_dict == {
'details': {
'key': u'value',
'integer': 37
}
}

def test_prompt_for_config_deep_dict(self, monkeypatch):
monkeypatch.setattr(
'cookiecutter.prompt.read_user_dict',
lambda var, default: {
"key": "value",
"integer_key": 37,
"dict_key": {
"deep_key": "deep_value",
"deep_integer": 42,
"deep_list": [
"deep value 1",
"deep value 2",
"deep value 3",
]
},
"list_key": [
"value 1",
"value 2",
"value 3",
]
}
)
context = {'cookiecutter': {'details': {}}}

cookiecutter_dict = prompt.prompt_for_config(context)
assert cookiecutter_dict == {
'details': {
"key": "value",
"integer_key": 37,
"dict_key": {
"deep_key": "deep_value",
"deep_integer": 42,
"deep_list": [
"deep value 1",
"deep value 2",
"deep value 3",
]
},
"list_key": [
"value 1",
"value 2",
"value 3",
]
}
}

def test_should_render_dict(self):
context = {
'cookiecutter': {
'project_name': 'Slartibartfast',
'details': {
'other_name': '{{cookiecutter.project_name}}'
}
}
}

cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
assert cookiecutter_dict == {
'project_name': 'Slartibartfast',
'details': {
'other_name': u'Slartibartfast',
}
}

def test_should_render_deep_dict(self):
context = {
'cookiecutter': {
'project_name': "Slartibartfast",
'details': {
"key": "value",
"integer_key": 37,
"other_name": '{{cookiecutter.project_name}}',
"dict_key": {
"deep_key": "deep_value",
"deep_integer": 42,
"deep_other_name": '{{cookiecutter.project_name}}',
"deep_list": [
"deep value 1",
"{{cookiecutter.project_name}}",
"deep value 3",
]
},
"list_key": [
"value 1",
"{{cookiecutter.project_name}}",
"value 3",
]
}
}
}

cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
assert cookiecutter_dict == {
'project_name': "Slartibartfast",
'details': {
"key": "value",
"integer_key": "37",
"other_name": "Slartibartfast",
"dict_key": {
"deep_key": "deep_value",
"deep_integer": "42",
"deep_other_name": "Slartibartfast",
"deep_list": [
"deep value 1",
"Slartibartfast",
"deep value 3",
]
},
"list_key": [
"value 1",
"Slartibartfast",
"value 3",
]
}
}

def test_unicode_prompt_for_config_unicode(self, monkeypatch):
monkeypatch.setattr(
'cookiecutter.prompt.read_user_variable',
Expand Down Expand Up @@ -279,3 +420,33 @@ def test_undefined_variable_in_cookiecutter_dict_with_choices():
error = err.value
assert error.message == "Unable to render variable 'foo'"
assert error.context == context


def test_undefined_variable_in_cookiecutter_dict_with_dict_key():
context = {
'cookiecutter': {
'hello': 'world',
'foo': {'{{cookiecutter.nope}}': 'value'}
}
}
with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
prompt.prompt_for_config(context, no_input=True)

error = err.value
assert error.message == "Unable to render variable 'foo'"
assert error.context == context


def test_undefined_variable_in_cookiecutter_dict_with_key_value():
context = {
'cookiecutter': {
'hello': 'world',
'foo': {'key': '{{cookiecutter.nope}}'}
}
}
with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
prompt.prompt_for_config(context, no_input=True)

error = err.value
assert error.message == "Unable to render variable 'foo'"
assert error.context == context
Loading
0