Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ Deprecated
- ``skip_null`` flag for ``--print_config`` was deprecated and will be removed
in v5.0.0. Use ``skip_unset`` instead (`#909
<https://github.com/omni-us/jsonargparse/pull/909>`__).
- Implicit selection of subcommand when multiple subcommand settings available
is deprecated and will be removed in v5.0.0. Provide an explicit subcommand
instead (`#923 <https://github.com/omni-us/jsonargparse/pull/923>`__).


v4.49.0 (2026-05-15)
Expand Down
12 changes: 12 additions & 0 deletions jsonargparse/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,3 +1041,15 @@ def __post_init__(self):
arg.__post_init__(self)

return ComposedDataclass


def deprecated_implicit_subcommand(component, subcommand_keys: list[str], subcommand: str, dest: str):
deprecation_warning(
component,
(
f"Multiple subcommand settings provided ({', '.join(subcommand_keys)}) without an "
f"explicit '{dest}' key. Subcommand '{subcommand}' will be used. From v5.0.0 "
"providing an explicit subcommand will be required."
),
stacklevel=6,
)
8 changes: 3 additions & 5 deletions jsonargparse/_subcommands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Subcommands action and helper functions."""

import warnings
from argparse import Action as ArgparseAction
from argparse import _SubParsersAction
from contextlib import contextmanager
Expand All @@ -9,6 +8,7 @@

from ._actions import filter_non_parsing_actions
from ._common import parsing_defaults, single_subcommand
from ._deprecated import deprecated_implicit_subcommand
from ._namespace import Namespace, NSKeyError, split_key, split_key_root
from ._type_checking import ActionsContainer, ArgumentParser

Expand Down Expand Up @@ -198,10 +198,8 @@ def get_subcommands(
elif len(subcommand_keys) > 0 and (fail_no_subcommand or require_single):
cfg[dest] = subcommand = subcommand_keys[0]
if len(subcommand_keys) > 1:
warnings.warn(
f"Multiple subcommand settings provided ({', '.join(subcommand_keys)}) without an "
f"explicit '{dest}' key. Subcommand '{subcommand}' will be used."
)
deprecated_implicit_subcommand(get_subcommands, subcommand_keys, subcommand, dest)
# v5.0.0 replace deprecated_implicit_subcommand with raise ValueError

# Remove extra subcommand settings
if subcommand and len(subcommand_keys) > 1:
Expand Down
44 changes: 44 additions & 0 deletions jsonargparse_tests/test_deprecated.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import dataclasses
import json
import os
import pathlib
import sys
Expand Down Expand Up @@ -71,6 +72,7 @@
from jsonargparse_tests.test_jsonnet import example_2_jsonnet
from jsonargparse_tests.test_paths import paths # noqa: F401
from jsonargparse_tests.test_subclasses import CustomInstantiationBase, instantiator
from jsonargparse_tests.test_subcommands import subcommands_parser # noqa: F401


@pytest.fixture(autouse=True, scope="module")
Expand Down Expand Up @@ -1091,7 +1093,7 @@


def test_add_argument_enable_path_deprecated(parser, tmp_cwd):
import json

Check notice

Code scanning / CodeQL

Module is imported more than once Note test

This import of module json is redundant, as it was previously imported
on line 4
.

data = {"a": 1}
pathlib.Path("data.yaml").write_text(json.dumps(data))
Expand Down Expand Up @@ -1194,3 +1196,45 @@
message="skip_null flag for --print_config was deprecated",
code=None,
)


def test_subcommands_parse_string_first_implicit_subcommand(subcommands_parser): # noqa: F811
with catch_warnings(record=True) as w:
cfg = subcommands_parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
assert_deprecation_warn(
w,
message="Multiple subcommand settings provided",
code="cfg = subcommands_parser.parse_string(",
)
assert "Subcommand 'a' will be" in str(w[1].message)
assert cfg.subcommand == "a"
assert "b" not in cfg


def test_subcommands_implicit_in_default_config_files(parser, tmp_cwd):
parser.default_config_files = ["defaults.json"]
subs = parser.add_subcommands(required=True, dest="sub")
sub1 = ArgumentParser()
sub1.add_argument("--sub1val")
subs.add_subcommand("sub1", sub1)
sub2 = ArgumentParser()
sub2.add_argument("--sub2val")
subs.add_subcommand("sub2", sub2)

defaults: dict = {
"sub1": {"sub1val": 2},
"sub2": {"sub2val": 3},
}
pathlib.Path("defaults.json").write_text(json.dumps(defaults))

with catch_warnings(record=True) as w:
cfg = parser.parse_args([])
assert_deprecation_warn(
w,
message="Multiple subcommand settings provided",
code="cfg = parser.parse_args([])",
)
assert "Subcommand 'sub1' will be" in str(w[1].message)
assert cfg.sub == "sub1"
assert cfg.sub1 == Namespace(sub1val=2)
assert "sub2" not in cfg
18 changes: 0 additions & 18 deletions jsonargparse_tests/test_subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import json
import os
import warnings
from pathlib import Path
from unittest.mock import patch

Expand Down Expand Up @@ -166,15 +165,6 @@ def test_subcommands_parse_string_implicit_subcommand(subcommands_parser):
ctx.match("Subcommand 'a' does not accept option 'unk'")


def test_subcommands_parse_string_first_implicit_subcommand(subcommands_parser):
with warnings.catch_warnings(record=True) as w:
cfg = subcommands_parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
assert len(w) == 1
assert "Subcommand 'a' will be used" in str(w[0].message)
assert cfg.subcommand == "a"
assert "b" not in cfg


def test_subcommands_parse_string_explicit_subcommand(subcommands_parser):
cfg = subcommands_parser.parse_string('{"subcommand": "b", "a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
assert cfg.subcommand == "b"
Expand Down Expand Up @@ -529,14 +519,6 @@ def test_subcommands_in_default_config_files(parser, subtests, tmp_cwd):
assert cfg.sub2 == Namespace(sub2val=3)
assert "sub1" not in cfg

with subtests.test("implicit subcommand defaults"):
with warnings.catch_warnings(record=True) as w:
cfg = parser.parse_args([])
assert "Subcommand 'sub1' will be used" in str(w[0].message)
assert cfg.sub == "sub1"
assert cfg.sub1 == Namespace(sub1val=2)
assert "sub2" not in cfg

with subtests.test("no subcommand in defaults"):
defaults["sub"] = "sub2"
Path("defaults.json").write_text(json.dumps(defaults))
Expand Down
Loading