Skip to content

Commit

Permalink
Add support for click 8.x (cookiecutter#1569)
Browse files Browse the repository at this point in the history
* Make read_user_dict compatible with click 8.x

The `read_user_dict` function uses a "default" sentinel instead of the actual
default value. Being a JSON dict, the latter would often be hard to type.

Under click 8.x, the default value for `click.prompt` is passed to the
`read_proc` callback. We use this callback to load JSON from the user input, and
this would choke on an input like "default" (without quotes). Therefore, change
the callback to return the default value when it receives the "default"
sentinel.

Under click 7.x (which is our minimum version), the default value for
`click.prompt` is returned as-is. Therefore, continue to handle the case where
`click.prompt` returns "default" instead of the actual default value, but only
if we're actually running under click 7.x.

(Checking for click 7.x is only done for clarity. Under click 8.x,
`click.prompt` would never return "default", even if a user entered it as a
valid JSON string. This is because our callback requires a dict, not a string.)

* test: Expect read_user_dict to call click.prompt with partial object

Previously, tests for `read_user_dict` expected `process_json` to be passed to
click.prompt directly. Instead, we now pass an instance of `functools.partial`,
so adapt the mock to reflect that.

* test: Avoid mocking `click.prompt` when testing defaults

Do not mock `click.prompt` when testing that `read_user_dict` returns the proper
default value (rather than the sentinel "default"). Mocking `click.prompt`
prevents our callback from running, and under click >= 8.0 we process the
sentinel in the callback. Instead, use `click.testing.CliRunner` to fake
standard input.

* test: Adapt regression test for default handling

Expect `json.loads` not to be called with the sentinel ("default"). Previously,
the test expected `process_json` not to be called, but under click >= 8.0 that
is now where we handle the sentinel.

* Update dependencies for click 8.x
  • Loading branch information
cjolowicz authored Jun 15, 2021
1 parent a54de67 commit b1f6427
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 25 deletions.
23 changes: 16 additions & 7 deletions cookiecutter/prompt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Functions for prompting the user for project info."""
import functools
import json
from collections import OrderedDict

Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 17 additions & 17 deletions tests/test_read_user_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}

0 comments on commit b1f6427

Please sign in to comment.