TestPromptChoiceForConfig.test_should_return_first_option_if_no_input()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 16
rs 9.85
c 0
b 0
f 0
cc 1
nop 4
1
"""Tests for `cookiecutter.prompt` module."""
2
import platform
3
from collections import OrderedDict
4
5
import pytest
6
7
from cookiecutter import prompt, exceptions, environment
8
9
10
@pytest.fixture(autouse=True)
11
def patch_readline_on_win(monkeypatch):
12
    """Fixture. Overwrite windows end of line to linux standard."""
13
    if 'windows' in platform.platform().lower():
14
        monkeypatch.setattr('sys.stdin.readline', lambda: '\n')
15
16
17
class TestRenderVariable:
18
    """Class to unite simple and complex tests for render_variable function."""
19
20
    @pytest.mark.parametrize(
21
        'raw_var, rendered_var',
22
        [
23
            (1, '1'),
24
            (True, True),
25
            ('foo', 'foo'),
26
            ('{{cookiecutter.project}}', 'foobar'),
27
            (None, None),
28
        ],
29
    )
30
    def test_convert_to_str(self, mocker, raw_var, rendered_var):
31
        """Verify simple items correctly rendered to strings."""
32
        env = environment.StrictEnvironment()
33
        from_string = mocker.patch(
34
            'cookiecutter.prompt.StrictEnvironment.from_string', wraps=env.from_string
35
        )
36
        context = {'project': 'foobar'}
37
38
        result = prompt.render_variable(env, raw_var, context)
39
        assert result == rendered_var
40
41
        # Make sure that non None non str variables are converted beforehand
42
        if raw_var is not None and not isinstance(raw_var, bool):
43
            if not isinstance(raw_var, str):
44
                raw_var = str(raw_var)
45
            from_string.assert_called_once_with(raw_var)
46
        else:
47
            assert not from_string.called
48
49
    @pytest.mark.parametrize(
50
        'raw_var, rendered_var',
51
        [
52
            ({1: True, 'foo': False}, {'1': True, 'foo': False}),
53
            (
54
                {'{{cookiecutter.project}}': ['foo', 1], 'bar': False},
55
                {'foobar': ['foo', '1'], 'bar': False},
56
            ),
57
            (['foo', '{{cookiecutter.project}}', None], ['foo', 'foobar', None]),
58
        ],
59
    )
60
    def test_convert_to_str_complex_variables(self, raw_var, rendered_var):
61
        """Verify tree items correctly rendered."""
62
        env = environment.StrictEnvironment()
63
        context = {'project': 'foobar'}
64
65
        result = prompt.render_variable(env, raw_var, context)
66
        assert result == rendered_var
67
68
69
class TestPrompt:
70
    """Class to unite user prompt related tests."""
71
72
    @pytest.mark.parametrize(
73
        'context',
74
        [
75
            {'cookiecutter': {'full_name': 'Your Name'}},
76
            {'cookiecutter': {'full_name': 'Řekni či napiš své jméno'}},
77
        ],
78
        ids=['ASCII default prompt/input', 'Unicode default prompt/input'],
79
    )
80
    def test_prompt_for_config(self, monkeypatch, context):
81
        """Verify `prompt_for_config` call `read_user_variable` on text request."""
82
        monkeypatch.setattr(
83
            'cookiecutter.prompt.read_user_variable',
84
            lambda var, default: default,
85
        )
86
87
        cookiecutter_dict = prompt.prompt_for_config(context)
88
        assert cookiecutter_dict == context['cookiecutter']
89
90
    def test_prompt_for_config_dict(self, monkeypatch):
91
        """Verify `prompt_for_config` call `read_user_variable` on dict request."""
92
        monkeypatch.setattr(
93
            'cookiecutter.prompt.read_user_dict',
94
            lambda var, default: {"key": "value", "integer": 37},
95
        )
96
        context = {'cookiecutter': {'details': {}}}
97
98
        cookiecutter_dict = prompt.prompt_for_config(context)
99
        assert cookiecutter_dict == {'details': {'key': 'value', 'integer': 37}}
100
101
    def test_should_render_dict(self):
102
        """Verify template inside dictionary variable rendered."""
103
        context = {
104
            'cookiecutter': {
105
                'project_name': 'Slartibartfast',
106
                'details': {
107
                    '{{cookiecutter.project_name}}': '{{cookiecutter.project_name}}'
108
                },
109
            }
110
        }
111
112
        cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
113
        assert cookiecutter_dict == {
114
            'project_name': 'Slartibartfast',
115
            'details': {'Slartibartfast': 'Slartibartfast'},
116
        }
117
118
    def test_should_render_deep_dict(self):
119
        """Verify nested structures like dict in dict, rendered correctly."""
120
        context = {
121
            'cookiecutter': {
122
                'project_name': "Slartibartfast",
123
                'details': {
124
                    "key": "value",
125
                    "integer_key": 37,
126
                    "other_name": '{{cookiecutter.project_name}}',
127
                    "dict_key": {
128
                        "deep_key": "deep_value",
129
                        "deep_integer": 42,
130
                        "deep_other_name": '{{cookiecutter.project_name}}',
131
                        "deep_list": [
132
                            "deep value 1",
133
                            "{{cookiecutter.project_name}}",
134
                            "deep value 3",
135
                        ],
136
                    },
137
                    "list_key": [
138
                        "value 1",
139
                        "{{cookiecutter.project_name}}",
140
                        "value 3",
141
                    ],
142
                },
143
            }
144
        }
145
146
        cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
147
        assert cookiecutter_dict == {
148
            'project_name': "Slartibartfast",
149
            'details': {
150
                "key": "value",
151
                "integer_key": "37",
152
                "other_name": "Slartibartfast",
153
                "dict_key": {
154
                    "deep_key": "deep_value",
155
                    "deep_integer": "42",
156
                    "deep_other_name": "Slartibartfast",
157
                    "deep_list": ["deep value 1", "Slartibartfast", "deep value 3"],
158
                },
159
                "list_key": ["value 1", "Slartibartfast", "value 3"],
160
            },
161
        }
162
163
    def test_prompt_for_templated_config(self, monkeypatch):
164
        """Verify Jinja2 templating works in unicode prompts."""
165
        monkeypatch.setattr(
166
            'cookiecutter.prompt.read_user_variable', lambda var, default: default
167
        )
168
        context = {
169
            'cookiecutter': OrderedDict(
170
                [
171
                    ('project_name', 'A New Project'),
172
                    (
173
                        'pkg_name',
174
                        '{{ cookiecutter.project_name|lower|replace(" ", "") }}',
175
                    ),
176
                ]
177
            )
178
        }
179
180
        exp_cookiecutter_dict = {
181
            'project_name': 'A New Project',
182
            'pkg_name': 'anewproject',
183
        }
184
        cookiecutter_dict = prompt.prompt_for_config(context)
185
        assert cookiecutter_dict == exp_cookiecutter_dict
186
187
    def test_dont_prompt_for_private_context_var(self, monkeypatch):
188
        """Verify `read_user_variable` not called for private context variables."""
189
        monkeypatch.setattr(
190
            'cookiecutter.prompt.read_user_variable',
191
            lambda var, default: pytest.fail(
192
                'Should not try to read a response for private context var'
193
            ),
194
        )
195
        context = {'cookiecutter': {'_copy_without_render': ['*.html']}}
196
        cookiecutter_dict = prompt.prompt_for_config(context)
197
        assert cookiecutter_dict == {'_copy_without_render': ['*.html']}
198
199
    def test_should_render_private_variables_with_two_underscores(self):
200
        """Test rendering of private variables with two underscores.
201
202
        There are three cases:
203
        1. Variables beginning with a single underscore are private and not rendered.
204
        2. Variables beginning with a double underscore are private and are rendered.
205
        3. Variables beginning with anything other than underscores are not private and
206
           are rendered.
207
        """
208
        context = {
209
            'cookiecutter': OrderedDict(
210
                [
211
                    ('foo', 'Hello world'),
212
                    ('bar', 123),
213
                    ('rendered_foo', '{{ cookiecutter.foo|lower }}'),
214
                    ('rendered_bar', 123),
215
                    ('_hidden_foo', '{{ cookiecutter.foo|lower }}'),
216
                    ('_hidden_bar', 123),
217
                    ('__rendered_hidden_foo', '{{ cookiecutter.foo|lower }}'),
218
                    ('__rendered_hidden_bar', 123),
219
                ]
220
            )
221
        }
222
        cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
223
        assert cookiecutter_dict == OrderedDict(
224
            [
225
                ('foo', 'Hello world'),
226
                ('bar', '123'),
227
                ('rendered_foo', 'hello world'),
228
                ('rendered_bar', '123'),
229
                ('_hidden_foo', '{{ cookiecutter.foo|lower }}'),
230
                ('_hidden_bar', 123),
231
                ('__rendered_hidden_foo', 'hello world'),
232
                ('__rendered_hidden_bar', '123'),
233
            ]
234
        )
235
236
    def test_should_not_render_private_variables(self):
237
        """Verify private(underscored) variables not rendered by `prompt_for_config`.
238
239
        Private variables designed to be raw, same as context input.
240
        """
241
        context = {
242
            'cookiecutter': {
243
                'project_name': 'Skip render',
244
                '_skip_jinja_template': '{{cookiecutter.project_name}}',
245
                '_skip_float': 123.25,
246
                '_skip_integer': 123,
247
                '_skip_boolean': True,
248
                '_skip_nested': True,
249
            }
250
        }
251
        cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
252
        assert cookiecutter_dict == context['cookiecutter']
253
254
255
class TestReadUserChoice:
256
    """Class to unite choices prompt related tests."""
257
258
    def test_should_invoke_read_user_choice(self, mocker):
259
        """Verify correct function called for select(list) variables."""
260
        prompt_choice = mocker.patch(
261
            'cookiecutter.prompt.prompt_choice_for_config',
262
            wraps=prompt.prompt_choice_for_config,
263
        )
264
265
        read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
266
        read_user_choice.return_value = 'all'
267
268
        read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
269
270
        choices = ['landscape', 'portrait', 'all']
271
        context = {'cookiecutter': {'orientation': choices}}
272
273
        cookiecutter_dict = prompt.prompt_for_config(context)
274
275
        assert not read_user_variable.called
276
        assert prompt_choice.called
277
        read_user_choice.assert_called_once_with('orientation', choices)
278
        assert cookiecutter_dict == {'orientation': 'all'}
279
280
    def test_should_invoke_read_user_variable(self, mocker):
281
        """Verify correct function called for string input variables."""
282
        read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
283
        read_user_variable.return_value = 'Audrey Roy'
284
285
        prompt_choice = mocker.patch('cookiecutter.prompt.prompt_choice_for_config')
286
287
        read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
288
289
        context = {'cookiecutter': {'full_name': 'Your Name'}}
290
291
        cookiecutter_dict = prompt.prompt_for_config(context)
292
293
        assert not prompt_choice.called
294
        assert not read_user_choice.called
295
        read_user_variable.assert_called_once_with('full_name', 'Your Name')
296
        assert cookiecutter_dict == {'full_name': 'Audrey Roy'}
297
298
    def test_should_render_choices(self, mocker):
299
        """Verify Jinja2 templating engine works inside choices variables."""
300
        read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
301
        read_user_choice.return_value = 'anewproject'
302
303
        read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
304
        read_user_variable.return_value = 'A New Project'
305
306
        rendered_choices = ['foo', 'anewproject', 'bar']
307
308
        context = {
309
            'cookiecutter': OrderedDict(
310
                [
311
                    ('project_name', 'A New Project'),
312
                    (
313
                        'pkg_name',
314
                        [
315
                            'foo',
316
                            '{{ cookiecutter.project_name|lower|replace(" ", "") }}',
317
                            'bar',
318
                        ],
319
                    ),
320
                ]
321
            )
322
        }
323
324
        expected = {
325
            'project_name': 'A New Project',
326
            'pkg_name': 'anewproject',
327
        }
328
        cookiecutter_dict = prompt.prompt_for_config(context)
329
330
        read_user_variable.assert_called_once_with('project_name', 'A New Project')
331
        read_user_choice.assert_called_once_with('pkg_name', rendered_choices)
332
        assert cookiecutter_dict == expected
333
334
335
class TestPromptChoiceForConfig:
336
    """Class to unite choices prompt related tests with config test."""
337
338
    @pytest.fixture
339
    def choices(self):
340
        """Fixture. Just populate choices variable."""
341
        return ['landscape', 'portrait', 'all']
342
343
    @pytest.fixture
344
    def context(self, choices):
345
        """Fixture. Just populate context variable."""
346
        return {'cookiecutter': {'orientation': choices}}
347
348
    def test_should_return_first_option_if_no_input(self, mocker, choices, context):
349
        """Verify prompt_choice_for_config return first list option on no_input=True."""
350
        read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
351
352
        expected_choice = choices[0]
353
354
        actual_choice = prompt.prompt_choice_for_config(
355
            cookiecutter_dict=context,
356
            env=environment.StrictEnvironment(),
357
            key='orientation',
358
            options=choices,
359
            no_input=True,  # Suppress user input
360
        )
361
362
        assert not read_user_choice.called
363
        assert expected_choice == actual_choice
364
365
    def test_should_read_user_choice(self, mocker, choices, context):
366
        """Verify prompt_choice_for_config return user selection on no_input=False."""
367
        read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice')
368
        read_user_choice.return_value = 'all'
369
370
        expected_choice = 'all'
371
372
        actual_choice = prompt.prompt_choice_for_config(
373
            cookiecutter_dict=context,
374
            env=environment.StrictEnvironment(),
375
            key='orientation',
376
            options=choices,
377
            no_input=False,  # Ask the user for input
378
        )
379
        read_user_choice.assert_called_once_with('orientation', choices)
380
        assert expected_choice == actual_choice
381
382
383
class TestReadUserYesNo(object):
384
    """Class to unite boolean prompt related tests."""
385
386
    @pytest.mark.parametrize(
387
        'run_as_docker',
388
        (
389
            True,
390
            False,
391
        ),
392
    )
393
    def test_should_invoke_read_user_yes_no(self, mocker, run_as_docker):
394
        """Verify correct function called for boolean variables."""
395
        read_user_yes_no = mocker.patch('cookiecutter.prompt.read_user_yes_no')
396
        read_user_yes_no.return_value = run_as_docker
397
398
        read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable')
399
400
        context = {'cookiecutter': {'run_as_docker': run_as_docker}}
401
402
        cookiecutter_dict = prompt.prompt_for_config(context)
403
404
        assert not read_user_variable.called
405
        read_user_yes_no.assert_called_once_with('run_as_docker', run_as_docker)
406
        assert cookiecutter_dict == {'run_as_docker': run_as_docker}
407
408
    def test_boolean_parameter_no_input(self):
409
        """Verify boolean parameter sent to prompt for config with no input."""
410
        context = {
411
            'cookiecutter': {
412
                'run_as_docker': True,
413
            }
414
        }
415
        cookiecutter_dict = prompt.prompt_for_config(context, no_input=True)
416
        assert cookiecutter_dict == context['cookiecutter']
417
418
419
@pytest.mark.parametrize(
420
    'context',
421
    (
422
        {'cookiecutter': {'foo': '{{cookiecutter.nope}}'}},
423
        {'cookiecutter': {'foo': ['123', '{{cookiecutter.nope}}', '456']}},
424
        {'cookiecutter': {'foo': {'{{cookiecutter.nope}}': 'value'}}},
425
        {'cookiecutter': {'foo': {'key': '{{cookiecutter.nope}}'}}},
426
    ),
427
    ids=[
428
        'Undefined variable in cookiecutter dict',
429
        'Undefined variable in cookiecutter dict with choices',
430
        'Undefined variable in cookiecutter dict with dict_key',
431
        'Undefined variable in cookiecutter dict with key_value',
432
    ],
433
)
434
def test_undefined_variable(context):
435
    """Verify `prompt.prompt_for_config` raises correct error."""
436
    with pytest.raises(exceptions.UndefinedVariableInTemplate) as err:
437
        prompt.prompt_for_config(context, no_input=True)
438
439
    error = err.value
440
    assert error.message == "Unable to render variable 'foo'"
441
    assert error.context == context
442