diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index dfb8f3218..4b8b2fbe6 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -1,4 +1,5 @@ """Functions for prompting the user for project info.""" +import functools import json from collections import OrderedDict @@ -78,11 +79,18 @@ def read_user_choice(var_name, options): return choice_map[user_choice] -def process_json(user_value): +DEFAULT_DISPLAY = 'default' + + +def process_json(user_value, default_value=None): """Load user-supplied value as a JSON dict. :param str user_value: User-supplied value to load as a JSON dict """ + if user_value == DEFAULT_DISPLAY: + # Return the given default w/o any processing + return default_value + try: user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) except Exception: @@ -107,15 +115,16 @@ def read_user_dict(var_name, default_value): if not isinstance(default_value, dict): raise TypeError - default_display = 'default' - user_value = click.prompt( - var_name, default=default_display, type=click.STRING, value_proc=process_json + var_name, + default=DEFAULT_DISPLAY, + type=click.STRING, + value_proc=functools.partial(process_json, default_value=default_value), ) - if user_value == default_display: - # Return the given default w/o any processing - return default_value + if click.__version__.startswith("7.") and user_value == DEFAULT_DISPLAY: + # click 7.x does not invoke value_proc on the default value. + return default_value # pragma: no cover return user_value diff --git a/setup.cfg b/setup.cfg index 609c2dc11..b9f2cf7a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,7 +62,7 @@ setup_requires = install_requires = binaryornot>=0.4.4 Jinja2>=2.7,<4.0.0 - click>=7.0,<8.0.0 + click>=7.0,<9.0.0 pyyaml>=5.3.1 jinja2-time>=0.2.0 python-slugify>=4.0.0 diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index ccf632258..0ce50efc2 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -92,35 +92,35 @@ def test_should_call_prompt_with_process_json(mocker): read_user_dict('name', {'project_slug': 'pytest-plugin'}) - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, - ) + args, kwargs = mock_prompt.call_args + + assert args == ('name',) + assert kwargs['type'] == click.STRING + assert kwargs['default'] == 'default' + assert kwargs['value_proc'].func == process_json -def test_should_not_call_process_json_default_value(mocker, monkeypatch): - """Make sure that `process_json` is not called when using default value.""" - mock_process_json = mocker.patch('cookiecutter.prompt.process_json', autospec=True) +def test_should_not_load_json_from_sentinel(mocker): + """Make sure that `json.loads` is not called when using default value.""" + mock_json_loads = mocker.patch( + 'cookiecutter.prompt.json.loads', autospec=True, return_value={} + ) runner = click.testing.CliRunner() with runner.isolation(input="\n"): read_user_dict('name', {'project_slug': 'pytest-plugin'}) - mock_process_json.assert_not_called() + mock_json_loads.assert_not_called() -def test_read_user_dict_default_value(mocker): +@pytest.mark.parametrize("input", ["\n", "default\n"]) +def test_read_user_dict_default_value(mocker, input): """Make sure that `read_user_dict` returns the default value. Verify return of a dict variable rather than the display value. """ - mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value='default', - ) - - val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) - - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, - ) + runner = click.testing.CliRunner() + with runner.isolation(input=input): + val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) assert val == {'project_slug': 'pytest-plugin'}