Repository: cookiecutter/cookiecutter

Issue:
passing list to template
I would like to be able to pass a list to the templates. See the two code blocks at the end of this post for an example. Right now it appears that if you pass a list in the config object, it's read as a list of options for that key.

I know that you can use `str.split()` in the jinja2 template, but that's not a real solution, since it's impossible to "escape" the character that's used as the delimiter. What's the best solution here? I would prefer to be able to pass a list in the json object and call it a day, but obviously that doesn't work for the user input prompts.
- `cookiecutter.json`:

``` json
{
  "build_steps": [
     "do_something",
     "do_something_else"
  ]
}
```
- `Dockerfile`:

``` jinja2
FROM something

{% for step in cookiecutter.build_steps %}
RUN {{ step }}
{% endfor %}
```



Discussion:
Hi @fly, thank you for submitting an issue :bow:

I guess we need to rework the context config in order to support complex values and type checks. Currently lists are treated as _options_.

@hackebrot This is something I have a need for as well, and I might be motivated enough to look into implementing it (as I need this to support exported data types [Briefcase](https://github.com/pybee/briefcase)). Has any thought been given to a preferred way to handle this? 

The easiest option I can think of: Since JSON lists are already being used as "choices",  we could modify the interpretation of dictionaries to always be "deep" data structures, and then use a structure like:

```
"key": {
    "values": [1,2,3,4]
}
```

if you need a list. Yes, this introduces an extra layer into the data structure exposed to the template, but the way choices are handled has already painted this situation into a corner. I suppose this case could be handled with a special case in the data structure - e.g., if your dictionary has a single key called "list", the value is interpreted as a literal list, rather than a dictionary containing that list. However, this would be a bit of a magical hack IMHO. 

Regardless of whether the "list reduction hack" is used, the UI for dictionary handling could be supplemented by JSON parsing for the command line interface; If the cookiecutter.json specifies a dictionary as the default (even an empty dictionary), then the command line UI could parse the content as JSON, rather than treating it as a literal string.

An alternative option that would work for _my_ use case, but probably not for the general case, would be a flag to the API for cookiecutter that ensures that `extra_context` is passed through as literals (possibly only when `no_input=True` is passed in as well - so you can opt for a literal, pre-parsed interpretation of extra_context).

Do any of these options sound appealing?


Resolving patch:
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -8,6 +8,7 @@
 """
 
 from collections import OrderedDict
+import json
 
 import click
 from past.builtins import basestring
@@ -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)
@@ -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
@@ -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


Test patch:
diff --git a/tests/test_prompt.py b/tests/test_prompt.py
--- a/tests/test_prompt.py
+++ b/tests/test_prompt.py
@@ -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',
@@ -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
diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py
new file mode 100644
--- /dev/null
+++ b/tests/test_read_user_dict.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+
+"""
+test_read_user_dict
+-------------------
+"""
+
+from __future__ import unicode_literals
+
+import click
+import pytest
+
+from cookiecutter.prompt import read_user_dict
+
+
+def test_use_default_dict(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = 'default'
+
+    VARIABLE = 'var'
+    DEFAULT = {"key": 1}
+
+    assert read_user_dict(VARIABLE, DEFAULT) == {'key': 1}
+
+    click.prompt.assert_called_once_with(VARIABLE, default='default')
+
+
+def test_empty_input_dict(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = '{}'
+
+    VARIABLE = 'var'
+    DEFAULT = {"key": 1}
+
+    assert read_user_dict(VARIABLE, DEFAULT) == {}
+
+    click.prompt.assert_called_once_with(VARIABLE, default='default')
+
+
+def test_use_empty_default_dict(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = 'default'
+
+    VARIABLE = 'var'
+    DEFAULT = {}
+
+    assert read_user_dict(VARIABLE, DEFAULT) == {}
+
+    click.prompt.assert_called_once_with(VARIABLE, default='default')
+
+
+def test_shallow_dict(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = '{"key": 2}'
+
+    VARIABLE = 'var'
+    DEFAULT = {}
+
+    assert read_user_dict(VARIABLE, DEFAULT) == {'key': 2}
+
+    click.prompt.assert_called_once_with(VARIABLE, default='default')
+
+
+def test_deep_dict(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = '''{
+        "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"
+        ]
+    }'''
+
+    VARIABLE = 'var'
+    DEFAULT = {}
+
+    assert read_user_dict(VARIABLE, 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",
+        ]
+    }
+
+    click.prompt.assert_called_once_with(VARIABLE, default='default')
+
+
+def test_raise_if_value_is_not_dict():
+    with pytest.raises(TypeError):
+        read_user_dict('foo', 'NOT A LIST')
+
+
+def test_raise_if_value_not_valid_json(mocker):
+    prompt = mocker.patch('click.prompt')
+    prompt.return_value = '{'
+
+    with pytest.raises(ValueError):
+        read_user_dict('foo', {})