diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e38a17e0..7b47285e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,9 @@ Fixed - ``dump`` failing when a link target requires serialization and ``skip_link_targets=False`` (`#542 `__). +- ``default_config_files`` making parse fail for subcommands and nested subclass + types (`lightning-forums#5963 + `__). - Import path of inherited classmethod not resolving correctly (`lightning#19863 comment `__). diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py index e681c2d6..e2bb047d 100644 --- a/jsonargparse/_actions.py +++ b/jsonargparse/_actions.py @@ -580,12 +580,12 @@ def add_prefix(key): @contextmanager def parent_parsers_context(key, parser): prev = parent_parsers.get() - curr = prev + [(key, parser)] - t = parent_parsers.set(curr) + curr = [] if parser is None else prev + [(key, parser)] + token = parent_parsers.set(curr) try: yield finally: - parent_parsers.reset(t) + parent_parsers.reset(token) class _ActionSubCommands(_SubParsersAction): diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 30c3ca6c..19aeb9ec 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -39,6 +39,7 @@ _find_action, _find_parent_action, _is_action_value_list, + parent_parsers_context, remove_actions, ) from ._common import ( @@ -453,7 +454,7 @@ def skip_sub_defaults_apply(v): or (isinstance(v, dict) and any(is_subclass_spec(e) for e in v.values())) ) - with ActionTypeHint.sub_defaults_context(): + with ActionTypeHint.sub_defaults_context(), parent_parsers_context(None, None): parser._apply_actions(cfg, skip_fn=skip_sub_defaults_apply) @staticmethod diff --git a/jsonargparse_tests/test_subcommands.py b/jsonargparse_tests/test_subcommands.py index 117b49f7..a181bbb0 100644 --- a/jsonargparse_tests/test_subcommands.py +++ b/jsonargparse_tests/test_subcommands.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os import warnings from pathlib import Path @@ -255,6 +256,46 @@ def test_subcommand_required_arg_in_default_config(parser, subparser, tmp_cwd): assert strip_meta(cfg) == Namespace(output="test", prepare=Namespace(media="test"), subcommand="prepare") +class SubModel: + def __init__(self, p1: int, p2: str = "-"): + pass + + +class Model: + def __init__(self, submodel: SubModel): + self.submodel = submodel + + +def test_subcommand_default_config_add_subdefaults(parser, subparser, tmp_cwd): + config = { + "fit": { + "model": { + "class_path": f"{__name__}.Model", + "init_args": { + "submodel": { + "class_path": f"{__name__}.SubModel", + "init_args": {"p1": 1}, + } + }, + } + } + } + Path("config.json").write_text(json.dumps(config)) + parser.default_config_files = ["config.json"] + subcommands = parser.add_subcommands() + subparser = ArgumentParser() + subparser.add_argument("--model", type=Model, required=True) + subcommands.add_subcommand("fit", subparser) + cfg = parser.parse_args([]) + assert cfg.fit.model.class_path == f"{__name__}.Model" + assert list(cfg.fit.model.init_args.__dict__.keys()) == ["submodel"] + assert cfg.fit.model.init_args.submodel.class_path == f"{__name__}.SubModel" + assert cfg.fit.model.init_args.submodel.init_args == Namespace(p1=1, p2="-") + init = parser.instantiate_classes(cfg) + assert isinstance(init.fit.model, Model) + assert isinstance(init.fit.model.submodel, SubModel) + + def test_subsubcommands_parse_args(subtests): parser_s1_a = ArgumentParser(exit_on_error=False) parser_s1_a.add_argument("--os1a", default="os1a_def")