forked from pallets/click
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_defaults.py
More file actions
356 lines (285 loc) · 12.1 KB
/
test_defaults.py
File metadata and controls
356 lines (285 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import pytest
import click
from click import UNPROCESSED
@pytest.mark.parametrize(
("default", "type", "expected_output", "expected_type"),
[
(42, click.FLOAT, "42.0", float),
("42", click.INT, "42", int),
(1.5, click.STRING, "1.5", str),
("1.5", click.FLOAT, "1.5", float),
("true", click.BOOL, "True", bool),
("0", click.BOOL, "False", bool),
],
)
def test_basic_defaults(runner, default, type, expected_output, expected_type):
"""Smoke test: a single option's default is type-coerced.
This covers basic single-option default type coercion.
"""
@click.command()
@click.option("--foo", default=default, type=type)
def cli(foo):
assert isinstance(foo, expected_type)
click.echo(f"FOO:[{foo}]")
result = runner.invoke(cli, [])
assert not result.exception
assert f"FOO:[{expected_output}]" in result.output
def test_multiple_defaults(runner):
"""Smoke test: each element in a multiple-option default is type-coerced.
.. hint::
``test_options.py::test_good_defaults_for_multiple``
covers the structural default processing (``list`` to
``tuple``, various ``nargs``) exhaustively.
This test fills the gap of explicit
``type=click.FLOAT`` coercion on the elements.
"""
@click.command()
@click.option("--foo", default=[23, 42], type=click.FLOAT, multiple=True)
def cli(foo):
for item in foo:
assert isinstance(item, float)
click.echo(item)
result = runner.invoke(cli, [])
assert not result.exception
assert result.output.splitlines() == ["23.0", "42.0"]
def test_nargs_plus_multiple(runner):
"""Smoke test: option with ``nargs=2`` + ``multiple=True`` and a
tuple-of-tuples default.
.. hint::
``test_options.py::test_good_defaults_for_multiple``
expands this with many more edge cases with various
``nargs``/``multiple``/``default`` combinations.
An argument-specific equivalent is in
``test_arguments.py::test_good_defaults_for_nargs``.
"""
@click.command()
@click.option(
"--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
)
def cli(arg):
for a, b in arg:
click.echo(f"<{a:d}|{b:d}>")
result = runner.invoke(cli, [])
assert not result.exception
assert result.output.splitlines() == ["<1|2>", "<3|4>"]
def test_multiple_flag_default(runner):
"""Default default for flags when multiple=True should be empty tuple."""
@click.command
# flag due to secondary token
@click.option("-y/-n", multiple=True)
# flag due to is_flag
@click.option("-f", is_flag=True, multiple=True)
# flag due to flag_value
@click.option("-v", "v", flag_value=1, multiple=True)
@click.option("-q", "v", flag_value=-1, multiple=True)
def cli(y, f, v):
return y, f, v
result = runner.invoke(cli, standalone_mode=False)
assert result.return_value == ((), (), ())
result = runner.invoke(cli, ["-y", "-n", "-f", "-v", "-q"], standalone_mode=False)
assert result.return_value == ((True, False), (True,), (1, -1))
def test_flag_default_map(runner):
"""test flag with default map"""
@click.group()
def cli():
pass
@cli.command()
@click.option("--name/--no-name", is_flag=True, show_default=True, help="name flag")
def foo(name):
click.echo(name)
result = runner.invoke(cli, ["foo"])
assert "False" in result.output
result = runner.invoke(cli, ["foo", "--help"])
assert "default: no-name" in result.output
result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": True}})
assert "True" in result.output
result = runner.invoke(cli, ["foo", "--help"], default_map={"foo": {"name": True}})
assert "default: name" in result.output
def test_shared_param_prefers_first_default(runner):
"""The first ``default=True`` wins when multiple ``flag_value`` options share
a parameter name, regardless of which positional option carries it.
.. hint::
``test_basic.py::test_flag_value_dual_options`` and
``test_options.py::test_default_dual_option_callback`` are wider
parametrized sibling tests covering many more default-value types (``None``,
``UNSET``, strings, numbers) but always place the default on the first
option. This test complements them by exercising both placements.
"""
@click.command
@click.option("--red", "color", flag_value="red")
@click.option("--green", "color", flag_value="green", default=True)
def prefers_green(color):
click.echo(color)
@click.command
@click.option("--red", "color", flag_value="red", default=True)
@click.option("--green", "color", flag_value="green")
def prefers_red(color):
click.echo(color)
result = runner.invoke(prefers_green, [])
assert "green" in result.output
result = runner.invoke(prefers_green, ["--red"])
assert "red" in result.output
result = runner.invoke(prefers_red, [])
assert "red" in result.output
result = runner.invoke(prefers_red, ["--green"])
assert "green" in result.output
@pytest.mark.parametrize(
("default_map", "key", "expected"),
[
# Key present in default_map.
({"email": "a@b.com"}, "email", "a@b.com"),
# Key missing from default_map.
({"email": "a@b.com"}, "nonexistent", None),
# No default_map at all / empty default_map.
(None, "anything", None),
({}, "anything", None),
# Falsy values are returned as-is.
({"key": None}, "key", None),
({"key": 0}, "key", 0),
({"key": ""}, "key", ""),
({"key": False}, "key", False),
],
)
def test_lookup_default_returns_hides_sentinel(default_map, key, expected):
"""``lookup_default()`` should return ``None`` for missing keys, not :attr:`UNSET`.
Regression test for https://github.com/pallets/click/issues/3145.
"""
cmd = click.Command("test")
ctx = click.Context(cmd)
if default_map is not None:
ctx.default_map = default_map
assert ctx.lookup_default(key) == expected
def test_lookup_default_callable_in_default_map(runner):
"""A callable in ``default_map`` is invoked with ``call=True``
(the default) and returned as-is with ``call=False``.
Click uses both paths internally:
- ``get_default()`` passes ``call=False``,
- ``resolve_ctx()`` passes ``call=True``.
"""
factory = lambda: "lazy-value" # noqa: E731
# Unit-level: call=True invokes, call=False returns as-is.
cmd = click.Command("test")
ctx = click.Context(cmd)
ctx.default_map = {"name": factory}
assert ctx.lookup_default("name", call=True) == "lazy-value"
assert ctx.lookup_default("name", call=False) is factory
# Integration: the callable is invoked during value resolution.
@click.command()
@click.option("--name", default="original", show_default=True)
@click.pass_context
def cli(ctx, name):
click.echo(f"name={name!r}")
result = runner.invoke(cli, [], default_map={"name": factory})
assert not result.exception
assert "name='lazy-value'" in result.output
# Help rendering gets the callable via call=False, so it
# shows "(dynamic)" rather than invoking it.
result = runner.invoke(cli, ["--help"], default_map={"name": factory})
assert not result.exception
assert "(dynamic)" in result.output
@pytest.mark.parametrize(
("args", "default_map", "expected_value", "expected_source"),
[
# CLI arg wins over everything.
(["--name", "cli"], {"name": "mapped"}, "cli", "COMMANDLINE"),
# default_map overrides parameter default.
([], {"name": "mapped"}, "mapped", "DEFAULT_MAP"),
# Explicit None in default_map still counts as DEFAULT_MAP.
([], {"name": None}, None, "DEFAULT_MAP"),
# Falsy values in default_map are not confused with missing keys.
([], {"name": ""}, "", "DEFAULT_MAP"),
([], {"name": 0}, "0", "DEFAULT_MAP"),
# No default_map falls back to parameter default.
([], None, "original", "DEFAULT"),
],
)
def test_default_map_source(runner, args, default_map, expected_value, expected_source):
"""``get_parameter_source()`` reports the correct origin for a parameter
value across the resolution chain: CLI > default_map > parameter default.
"""
@click.command()
@click.option("--name", default="original")
@click.pass_context
def cli(ctx, name):
source = ctx.get_parameter_source("name")
click.echo(f"name={name!r} source={source.name}")
kwargs = {}
if default_map is not None:
kwargs["default_map"] = default_map
result = runner.invoke(cli, args, **kwargs)
assert not result.exception
assert f"name={expected_value!r}" in result.output
assert f"source={expected_source}" in result.output
def test_lookup_default_override_respected(runner):
"""A subclass override of ``lookup_default()`` should be called by Click
internals, not bypassed by a private method.
Reproduce exactly https://github.com/pallets/click/issues/3145 in which a
subclass that falls back to prefix-based lookup when the parent returns
``None``.
Previous attempts in https://github.com/pallets/click/pr/3199 were entirely
bypassing the user's overridded method.
"""
class CustomContext(click.Context):
def lookup_default(self, name, call=True):
default = super().lookup_default(name, call=call)
if default is not None:
return default
# Prefix-based fallback: look up "app" sub-dict for "app_email".
prefix = name.split("_", 1)[0]
group = getattr(self, "default_map", None) or {}
sub = group.get(prefix)
if isinstance(sub, dict):
return sub.get(name)
return default
@click.command("get-views")
@click.option("--app-email", default="original", show_default=True)
@click.pass_context
def cli(ctx, app_email):
click.echo(f"app_email={app_email!r}")
cli.context_class = CustomContext
default_map = {"app": {"app_email": "prefix@example.com"}}
# resolve_ctx path: the override provides the runtime value.
result = runner.invoke(cli, [], default_map=default_map)
assert not result.exception
assert "app_email='prefix@example.com'" in result.output
# get_default path: the override is also used when
# rendering --help with show_default=True.
result = runner.invoke(cli, ["--help"], default_map=default_map)
assert not result.exception
assert "prefix@example.com" in result.output
class _Marker:
"""Dummy callable used as a flag_value in default tests."""
pass
@pytest.mark.parametrize(
("default_map", "args", "expected"),
[
# No default_map: auto-aligned default returns the class, not an instance.
(None, [], _Marker),
# CLI flag always returns the class.
(None, ["--opt"], _Marker),
# Static value in default_map overrides the auto-aligned flag_value.
({"value": "from-map"}, [], "from-map"),
# Callable in default_map is still invoked (not suppressed by the fix).
({"value": lambda: "lazy-map"}, [], "lazy-map"),
# None in default_map overrides the flag_value.
({"value": None}, [], None),
# CLI arg wins over default_map.
({"value": "from-map"}, ["--opt"], _Marker),
],
)
def test_default_map_with_callable_flag_value(runner, default_map, args, expected):
"""``default_map`` entries should override the auto-aligned callable ``flag_value``,
and callable entries in ``default_map`` should still be invoked.
Verifies the fix for https://github.com/pallets/click/issues/3121 does not
break ``default_map`` precedence.
"""
@click.command()
@click.option("--opt", "value", flag_value=_Marker, type=UNPROCESSED, default=True)
def cli(value):
click.echo(repr(value), nl=False)
kwargs = {}
if default_map is not None:
kwargs["default_map"] = default_map
result = runner.invoke(cli, args, **kwargs)
assert result.exit_code == 0
assert result.output == repr(expected)