Skip to content

Commit dd4dc43

Browse files
committed
Split string values from default_map for multi-value parameters
Fixes #2745
1 parent 8bd8b4a commit dd4dc43

4 files changed

Lines changed: 74 additions & 0 deletions

File tree

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Released 2026-04-20
4040
- Change :class:`ParameterSource` to an :class:`~enum.IntEnum` and reorder
4141
its members from most to least explicit, so values can be compared to
4242
check whether a parameter was explicitly provided. :issue:`2879` :pr:`3248`
43+
- Split string values from ``default_map`` for parameters with ``nargs > 1``
44+
or :class:`Tuple` type, matching environment variable behavior.
45+
:issue:`2745` :pr:`3364`
4346

4447
Version 8.3.2
4548
-------------

docs/commands.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,35 @@ And in action:
351351
})
352352
```
353353

354+
### Multi-value parameters
355+
356+
When a `default_map` value is a string for a parameter with `nargs > 1` or a
357+
{class}`Tuple` type, the string is split automatically, the same way an
358+
environment variable would be. By default, values are split on whitespace. See
359+
[Multiple Options from Environment
360+
Values](options.md#multiple-options-from-environment-values) for details on
361+
splitting behavior.
362+
363+
```python
364+
default_map = {
365+
"draw": {
366+
"point": "3 4", # split into ("3", "4") for nargs=2
367+
"color": "red", # passed as-is for nargs=1
368+
}
369+
}
370+
```
371+
372+
You can also pass an already-structured tuple or list, which will be used as-is
373+
without splitting:
374+
375+
```python
376+
default_map = {
377+
"draw": {
378+
"point": (3, 4), # used directly
379+
}
380+
}
381+
```
382+
354383
## Context Defaults
355384

356385
```{versionadded} 2.0

src/click/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,11 @@ def consume_value(
23562356
value = default_map_value
23572357
source = ParameterSource.DEFAULT_MAP
23582358

2359+
# A string from default_map must be split for multi-value
2360+
# parameters, matching value_from_envvar behavior.
2361+
if isinstance(value, str) and self.nargs != 1:
2362+
value = self.type.split_envvar_value(value)
2363+
23592364
if value is UNSET:
23602365
default_value = self.get_default(ctx)
23612366
if default_value is not UNSET:

tests/test_defaults.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,43 @@ def cli(value):
357357
assert result.output == repr(expected)
358358

359359

360+
@pytest.mark.parametrize(
361+
("default_map", "option_kwargs", "cli_args", "expected"),
362+
[
363+
# String is split for nargs=2 option.
364+
({"point": "3 4"}, {"nargs": 2, "type": int}, [], (3, 4)),
365+
# String is split for explicit Tuple type.
366+
({"point": "hello world"}, {"type": (str, str)}, [], ("hello", "world")),
367+
# Already-structured tuple passes through unchanged.
368+
({"point": ("a", "b")}, {"nargs": 2}, [], ("a", "b")),
369+
# Already-structured list passes through unchanged.
370+
({"point": [5, 6]}, {"nargs": 2, "type": int}, [], (5, 6)),
371+
# CLI args override default_map for nargs > 1.
372+
(
373+
{"point": "3 4"},
374+
{"nargs": 2, "type": int},
375+
["--point", "10", "20"],
376+
(10, 20),
377+
),
378+
],
379+
)
380+
def test_default_map_nargs(runner, default_map, option_kwargs, cli_args, expected):
381+
"""A string in ``default_map`` for an option with ``nargs > 1`` should be
382+
split the same way an environment variable string is split.
383+
384+
Regression test for https://github.com/pallets/click/issues/2745.
385+
"""
386+
387+
@click.command()
388+
@click.option("--point", **option_kwargs)
389+
def cli(point):
390+
click.echo(repr(point))
391+
392+
result = runner.invoke(cli, cli_args, default_map=default_map)
393+
assert result.exit_code == 0
394+
assert result.output.strip() == repr(expected)
395+
396+
360397
def test_unset_in_default_map(runner):
361398
"""An ``UNSET`` value in ``default_map`` should be treated as if
362399
the key is absent, and so fallback to the parameter's own default.

0 commit comments

Comments
 (0)