From 3a3e0350b6a2ea3e799440d48e779acabcf44de9 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Tue, 21 Apr 2026 16:58:25 +0200 Subject: [PATCH 1/2] Split string values from `default_map` for multi-value parameters Fixes #2745 --- CHANGES.rst | 3 +++ docs/commands.md | 29 +++++++++++++++++++++++++++++ docs/conf.py | 1 + src/click/core.py | 5 +++++ tests/test_defaults.py | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5068775906..a2e2089d29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -40,6 +40,9 @@ Released 2026-04-20 - Change :class:`ParameterSource` to an :class:`~enum.IntEnum` and reorder its members from most to least explicit, so values can be compared to check whether a parameter was explicitly provided. :issue:`2879` :pr:`3248` +- Split string values from ``default_map`` for parameters with ``nargs > 1`` + or :class:`Tuple` type, matching environment variable behavior. + :issue:`2745` :pr:`3364` Version 8.3.2 ------------- diff --git a/docs/commands.md b/docs/commands.md index 9f4c6e8f9e..ba064f515a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -351,6 +351,35 @@ And in action: }) ``` +### Multi-value parameters + +When a `default_map` value is a string for a parameter with `nargs > 1` or a +{class}`Tuple` type, the string is split automatically, the same way an +environment variable would be. By default, values are split on whitespace. See +[Multiple Options from Environment +Values](options.md#multiple-options-from-environment-values) for details on +splitting behavior. + +```python +default_map = { + "draw": { + "point": "3 4", # split into ("3", "4") for nargs=2 + "color": "red", # passed as-is for nargs=1 + } +} +``` + +You can also pass an already-structured tuple or list, which will be used as-is +without splitting: + +```python +default_map = { + "draw": { + "point": (3, 4), # used directly + } +} +``` + ## Context Defaults ```{versionadded} 2.0 diff --git a/docs/conf.py b/docs/conf.py index 2b223ab422..d04b783274 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,6 +31,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), } +myst_heading_anchors = 3 # HTML ----------------------------------------------------------------- diff --git a/src/click/core.py b/src/click/core.py index 65bdac5253..1cc9474d22 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2356,6 +2356,11 @@ def consume_value( value = default_map_value source = ParameterSource.DEFAULT_MAP + # A string from default_map must be split for multi-value + # parameters, matching value_from_envvar behavior. + if isinstance(value, str) and self.nargs != 1: + value = self.type.split_envvar_value(value) + if value is UNSET: default_value = self.get_default(ctx) if default_value is not UNSET: diff --git a/tests/test_defaults.py b/tests/test_defaults.py index cec051c67a..49f0ac6ed0 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -357,6 +357,43 @@ def cli(value): assert result.output == repr(expected) +@pytest.mark.parametrize( + ("default_map", "option_kwargs", "cli_args", "expected"), + [ + # String is split for nargs=2 option. + ({"point": "3 4"}, {"nargs": 2, "type": int}, [], (3, 4)), + # String is split for explicit Tuple type. + ({"point": "hello world"}, {"type": (str, str)}, [], ("hello", "world")), + # Already-structured tuple passes through unchanged. + ({"point": ("a", "b")}, {"nargs": 2}, [], ("a", "b")), + # Already-structured list passes through unchanged. + ({"point": [5, 6]}, {"nargs": 2, "type": int}, [], (5, 6)), + # CLI args override default_map for nargs > 1. + ( + {"point": "3 4"}, + {"nargs": 2, "type": int}, + ["--point", "10", "20"], + (10, 20), + ), + ], +) +def test_default_map_nargs(runner, default_map, option_kwargs, cli_args, expected): + """A string in ``default_map`` for an option with ``nargs > 1`` should be + split the same way an environment variable string is split. + + Regression test for https://github.com/pallets/click/issues/2745. + """ + + @click.command() + @click.option("--point", **option_kwargs) + def cli(point): + click.echo(repr(point)) + + result = runner.invoke(cli, cli_args, default_map=default_map) + assert result.exit_code == 0 + assert result.output.strip() == repr(expected) + + def test_unset_in_default_map(runner): """An ``UNSET`` value in ``default_map`` should be treated as if the key is absent, and so fallback to the parameter's own default. From 94004f1b5a4a982e8e33ef8d5f00cfb0e1dabddd Mon Sep 17 00:00:00 2001 From: Rowlando13 <67291205+Rowlando13@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:37:00 -0700 Subject: [PATCH 2/2] Put changes in correct section --- CHANGES.rst | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a2e2089d29..2e0752cc56 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,26 @@ .. currentmodule:: click +Version 8.4.0 +------------- + +Unreleased + +- :class:`ParamType` typing improvements. :pr:`3371` + + - :class:`ParamType` is now a generic abstract base class, + parameterized by its converted value type. + - :meth:`~ParamType.convert` return types are narrowed on all + concrete types (``str`` for :class:`STRING`, ``int`` for + :class:`INT`, etc.). + - :meth:`~ParamType.to_info_dict` returns specific + :class:`~typing.TypedDict` subclasses instead of + ``dict[str, Any]``. + - :class:`CompositeParamType` and the number-range base are now + generic with abstract methods. +- Split string values from ``default_map`` for parameters with ``nargs > 1`` + or :class:`Tuple` type, matching environment variable behavior. + :issue:`2745` :pr:`3364` + Version 8.3.3 ------------- @@ -40,9 +61,6 @@ Released 2026-04-20 - Change :class:`ParameterSource` to an :class:`~enum.IntEnum` and reorder its members from most to least explicit, so values can be compared to check whether a parameter was explicitly provided. :issue:`2879` :pr:`3248` -- Split string values from ``default_map`` for parameters with ``nargs > 1`` - or :class:`Tuple` type, matching environment variable behavior. - :issue:`2745` :pr:`3364` Version 8.3.2 -------------