Repository: omni-us/jsonargparse (python)

Issue:
Question about parse a list of dict
I'm trying to parse a arg that is a list of dict use the comand line tool:

The yaml: 
```yaml
# part of the config.yaml
model:
    augmentations:
    - a: random_crop
    - b: random_blur
```
Expected result:
```yaml
# part of the config.yaml
model:
    augmentations:
    - c: random_mosaic
    - d: random_perspective
```

The command line arg I try:
1.
```bash
--model.augmentations+={"c":"random_mosaic"} --model.augmentations+={"d":"random_perspective"}

#Error message
    Parser key "augmentations":
      Expected a <class 'dict'>. Got value: {c:random_mosaic}
```
2.
```bash
--model.augmentations=[{"c":"random_mosaic"},{"d":"random_perspective"}]

#Error message
    Parser key "augmentations":
      Expected a <class 'str'>. Got value: None
```

How to properly parse a list of dict use cli?


Pull request:
Fix list append nested in subclass not working
## What does this PR do?

Fixes #421

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [n/a] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ba75c85..3ff3e92 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -38,6 +38,8 @@ Fixed
   <https://github.com/omni-us/jsonargparse/pull/707>`__).
 - Nested generic dataclasses not working correctly (`#709
   <https://github.com/omni-us/jsonargparse/pull/709>`__).
+- List append nested in subclass not working (`#710
+  <https://github.com/omni-us/jsonargparse/pull/710>`__).
 
 
 v4.38.0 (2025-03-26)
diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py
index f4ba852..91bc824 100644
--- a/jsonargparse/_actions.py
+++ b/jsonargparse/_actions.py
@@ -64,7 +64,7 @@ def _find_action_and_subcommand(
         actions = [a for a in actions if not isinstance(a, exclude)]
     fallback_action = None
     for action in actions:
-        if action.dest == dest:
+        if action.dest == dest or f"--{dest}" in action.option_strings:
             if isinstance(action, _ActionConfigLoad):
                 fallback_action = action
             else:
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index f38bdb6..f48c920 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -953,6 +953,9 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
     ## Methods related to defaults ##
 
     def _get_default_config_files(self) -> List[Tuple[Optional[str], Path]]:
+        if getattr(self, "_inner_parser", False):
+            return []
+
         default_config_files = []
 
         for key, parser in parent_parsers.get():
@@ -1022,7 +1025,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
             if not default_config_file_content.strip():
                 continue
             with change_to_path_dir(default_config_file), parser_context(parent_parser=self):
-                cfg_file = self._load_config_parser_mode(default_config_file.get_content(), key=key)
+                cfg_file = self._load_config_parser_mode(default_config_file_content, key=key, prev_cfg=cfg)
                 cfg = self.merge_config(cfg_file, cfg)
                 try:
                     with _ActionPrintConfig.skip_print_config():
@@ -1365,11 +1368,15 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                 continue
 
             action_dest = action.dest if subcommand is None else subcommand + "." + action.dest
+            append = False
+            if action_dest not in cfg and key.endswith("+"):
+                append = True
+                cfg[action_dest] = cfg.pop(key)
             value = cfg[action_dest]
             if skip_fn and skip_fn(value):
                 continue
             with parser_context(parent_parser=self, lenient_check=True):
-                value = self._check_value_key(action, value, action_dest, prev_cfg)
+                value = self._check_value_key(action, value, action_dest, prev_cfg, append=append)
             if isinstance(action, _ActionConfigLoad):
                 config_keys.add(action_dest)
                 keys.append(action_dest)
@@ -1393,10 +1400,11 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         with parser_context(parent_parser=self):
             ActionTypeHint.discard_init_args_on_class_path_change(self, cfg_to, cfg_from)
         cfg_to.update(cfg_from)
-        ActionTypeHint.apply_appends(self, cfg_to)
         return cfg_to
 
-    def _check_value_key(self, action: argparse.Action, value: Any, key: str, cfg: Optional[Namespace]) -> Any:
+    def _check_value_key(
+        self, action: argparse.Action, value: Any, key: str, cfg: Optional[Namespace], append: bool = False
+    ) -> Any:
         """Checks the value for a given action.
 
         Args:
@@ -1421,7 +1429,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                 value = action.check_type(value, self)
         elif hasattr(action, "_check_type"):
             with parser_context(parent_parser=self):
-                value = action._check_type_(value, cfg=cfg)  # type: ignore[attr-defined]
+                value = action._check_type_(value, cfg=cfg, append=append)  # type: ignore[attr-defined]
         elif action.type is not None:
             try:
                 if action.nargs in {None, "?"} or action.nargs == 0:
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index 015853c..f55b0c0 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -43,6 +43,7 @@ from ._actions import (
     _find_parent_action,
     _is_action_value_list,
     parent_parsers_context,
+    parse_kwargs,
     remove_actions,
 )
 from ._common import (
@@ -484,16 +485,6 @@ class ActionTypeHint(Action):
             )
         )
 
-    @staticmethod
-    def apply_appends(parser, cfg):
-        for key in [k for k in cfg.keys() if k.endswith("+")]:
-            action = _find_action(parser, key[:-1])
-            if ActionTypeHint.supports_append(action):
-                with parser_context(load_value_mode=parser.parser_mode):
-                    val = action._check_type_(cfg[key], append=True, cfg=cfg)
-                cfg[key[:-1]] = val
-                cfg.pop(key)
-
     def serialize(self, value, dump_kwargs=None):
         sub_add_kwargs = getattr(self, "sub_add_kwargs", {})
         with dump_kwargs_context(dump_kwargs):
@@ -1058,10 +1049,14 @@ def adapt_typehints(
         if serialize and isinstance(val, str):
             return val
 
-        val_input = val
+        prev_implicit_defaults = False
         if prev_val is None and not inspect.isabstract(typehint) and not is_protocol(typehint):
             with suppress(ValueError):
                 prev_val = Namespace(class_path=get_import_path(typehint))  # implicit class_path
+                if parse_kwargs.get().get("defaults") is True:
+                    prev_implicit_defaults = True
+
+        val_input = val
         val = subclass_spec_as_namespace(val, prev_val)
         if not is_subclass_spec(val):
             msg = "Does not implement protocol" if is_protocol(typehint) else "Not a valid subclass of"
@@ -1088,6 +1083,9 @@ def adapt_typehints(
                     return_type = get_return_type(val_class, logger)
                     if is_subclass_or_implements_protocol(return_type, typehint):
                         not_subclass = False
+            elif prev_implicit_defaults:
+                inner_parser = ActionTypeHint.get_class_parser(typehint, sub_add_kwargs)
+                prev_val.init_args = inner_parser.get_defaults()
             if not_subclass:
                 msg = "implement protocol" if is_protocol(typehint) else "correspond to a subclass of"
                 raise_unexpected_value(f"Import path {val['class_path']} does not {msg} {typehint.__name__}")


Test patch:
diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py
index 1bdf5e4..7bb4ff2 100644
--- a/jsonargparse_tests/test_subclasses.py
+++ b/jsonargparse_tests/test_subclasses.py
@@ -1080,7 +1080,9 @@ def test_subclass_discard_init_args_with_default_config_files(parser, tmp_cwd, l
         cfg = parser.parse_args(['--cal={"class_path": "calendar.Calendar", "init_args": {"firstweekday": 3}}'])
     assert "discarding init_args: {'param': '1'}" in logs.getvalue()
     assert cfg.cal.init_args == Namespace(firstweekday=3)
-    assert type(parser.instantiate_classes(cfg).cal) is Calendar
+    with capture_logs(logger) as logs:
+        assert type(parser.instantiate_classes(cfg).cal) is Calendar
+    assert logs.getvalue()
 
 
 class Arch:
@@ -1165,7 +1167,9 @@ def test_discard_init_args_config_nested(parser, logger, tmp_cwd, method):
     with capture_logs(logger) as logs:
         cfg = parser.parse_args([f"--cfg={config_path}"])
     assert "discarding init_args: {'s1': 'x'}" in logs.getvalue()
-    init = parser.instantiate_classes(cfg)
+    with capture_logs(logger) as logs:
+        init = parser.instantiate_classes(cfg)
+    assert logs.getvalue()
     assert isinstance(init.main, ConfigDiscardMain)
     assert isinstance(init.main.sub, ConfigDiscardSub2)
 
@@ -1216,7 +1220,9 @@ def test_subclass_discard_init_args_dict_looks_like_subclass(parser, logger, tmp
     with capture_logs(logger) as logs:
         cfg = parser.parse_args([f"--cfg={config_paths[1]}", f"--cfg={config_paths[2]}"])
     assert "discarding init_args: {'s1': 1}" in logs.getvalue()
-    init = parser.instantiate_classes(cfg)
+    with capture_logs(logger) as logs:
+        init = parser.instantiate_classes(cfg)
+    assert logs.getvalue()
     assert isinstance(init.main, DictDiscardMain)
     assert isinstance(init.main.sub, dict)
     assert init.main.sub["init_args"]["s2"] == 2
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index d08601d..754f52f 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -499,6 +499,32 @@ def test_list_append_subcommand_subparser_default_config_files(parser, subparser
     assert cfg.sub.nums == [2]
 
 
+class NestedList:
+    def __init__(
+        self,
+        nested: List[Dict[str, str]] = [{"a": "random_crop"}, {"b": "random_blur"}],
+    ) -> None:
+        self.nested = nested
+
+
+def test_list_append_dicts_nested_with_default(parser):
+    parser.add_argument("--cls", type=NestedList, default={"class_path": "NestedList"})
+    cfg = parser.parse_args(['--cls.nested+={"d":"random_perspective"}'])
+    assert cfg.cls.class_path == f"{__name__}.NestedList"
+    assert cfg.cls.init_args == Namespace(
+        nested=[{"a": "random_crop"}, {"b": "random_blur"}, {"d": "random_perspective"}]
+    )
+
+
+def test_list_append_dicts_nested_without_default(parser):
+    parser.add_argument("--cls", type=NestedList)
+    cfg = parser.parse_args(["--cls=NestedList", '--cls.nested+={"d":"random_perspective"}'])
+    assert cfg.cls.class_path == f"{__name__}.NestedList"
+    assert cfg.cls.init_args == Namespace(
+        nested=[{"a": "random_crop"}, {"b": "random_blur"}, {"d": "random_perspective"}]
+    )
+
+
 # dict tests