Repository: omni-us/jsonargparse
Pull request #199 (2022-11-28 06:27:06)

Fix init_args not discarded for nested subclasses provided through command line
## What does this PR do?

Fixes https://github.com/Lightning-AI/lightning/issues/15796

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/master/CONTRIBUTING.rst)?
- [x] 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)


codecov-commenter: # [Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/199?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
Base: **100.00**% // Head: **100.00**% // No change to project coverage :thumbsup:
> Coverage data is based on head [(`7e45ca9`)](https://codecov.io/gh/omni-us/jsonargparse/pull/199?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) compared to base [(`f3af313`)](https://codecov.io/gh/omni-us/jsonargparse/commit/f3af313c1f4d6db59543225df3b793426186e83e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            master      #199   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         4592      4598    +6     
=========================================
+ Hits          4592      4598    +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| py3.10 | `91.56% <97.72%> (+0.03%)` | :arrow_up: |
| py3.10_all | `99.93% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.6 | `91.20% <97.61%> (+0.03%)` | :arrow_up: |
| py3.6_all | `99.64% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.7 | `91.22% <90.47%> (-0.04%)` | :arrow_down: |
| py3.7_all | `99.84% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.8 | `91.53% <97.72%> (+0.03%)` | :arrow_up: |
| py3.8_all | `99.91% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.9 | `91.56% <97.72%> (+0.03%)` | :arrow_up: |
| py3.9_all | `99.91% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/omni-us/jsonargparse/pull/199?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/core.py](https://codecov.io/gh/omni-us/jsonargparse/pull/199/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL2NvcmUucHk=) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/namespace.py](https://codecov.io/gh/omni-us/jsonargparse/pull/199/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL25hbWVzcGFjZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/optionals.py](https://codecov.io/gh/omni-us/jsonargparse/pull/199/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL29wdGlvbmFscy5weQ==) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/typehints.py](https://codecov.io/gh/omni-us/jsonargparse/pull/199/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL3R5cGVoaW50cy5weQ==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/199?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=199)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199&metric=new_duplicated_lines_density&view=list)


lgtm-com[bot]: This pull request **introduces 1 alert** when merging 7e45ca9eafcd1c33593b41aec65852228d96f502 into f3af313c1f4d6db59543225df3b793426186e83e - [view on LGTM.com](https://lgtm.com/projects/g/omni-us/jsonargparse/rev/pr-a8dafdf0c8bfd35931744d47174a6896c21cb532)

**new alerts:**

* 1 for Unused local variable
---

**Heads-up: LGTM.com's PR analysis will be disabled on the 5th of December**, and LGTM.com will be shut down ⏻ completely on the 16th of December 2022. Please enable GitHub code scanning, which uses the same CodeQL engine :gear: that powers LGTM.com. For more information, please check out [our post on the GitHub blog](https://github.blog/2022-08-15-the-next-step-for-lgtm-com-github-code-scanning/).
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=199)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=199&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=199&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f0d7469..a39d92d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -33,6 +33,9 @@ Fixed
   <https://github.com/Lightning-AI/lightning/issues/15256>`__.
 - Specifying only the class name through command line not working for
   ``Callable`` with class return type.
+- ``init_args`` not discarded for nested subclasses provided through command
+  line `pytorch-lightning#15796
+  <https://github.com/Lightning-AI/lightning/issues/15796>`__.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/core.py b/jsonargparse/core.py
index 04472f6..f2d84e1 100644
--- a/jsonargparse/core.py
+++ b/jsonargparse/core.py
@@ -299,9 +299,6 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
             with lenient_check_context():
                 ActionTypeHint.add_sub_defaults(self, cfg)
 
-        if not (with_meta or (with_meta is None and self._default_meta)):
-            cfg = strip_meta(cfg)
-
         _ActionPrintConfig.print_config_if_requested(self, cfg)
 
         with load_value_context(self.parser_mode):
@@ -310,6 +307,9 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
             if not skip_check and not lenient_check.get():
                 self.check_config(cfg, skip_required=skip_required)
 
+        if not (with_meta or (with_meta is None and self._default_meta)):
+            cfg = strip_meta(cfg)
+
         if log_message is not None:
             self._logger.info(log_message)
 
@@ -699,7 +699,6 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
         """
         check_valid_dump_format(format)
 
-        cfg = cfg.clone()
         cfg = strip_meta(cfg)
         ActionLink.strip_link_target_keys(self, cfg)
 
diff --git a/jsonargparse/namespace.py b/jsonargparse/namespace.py
index bc3a858..d405189 100644
--- a/jsonargparse/namespace.py
+++ b/jsonargparse/namespace.py
@@ -28,9 +28,7 @@ def split_key_leaf(key: str) -> List[str]:
     return key.rsplit('.', 1)
 
 
-def is_meta_key(key: Union[str, int]) -> bool:
-    if not isinstance(key, str):
-        return False
+def is_meta_key(key: str) -> bool:
     leaf_key = split_key_leaf(key)[-1]
     return leaf_key in meta_keys
 
@@ -50,31 +48,20 @@ def strip_meta(cfg):
     Returns:
         A copy of the configuration object excluding all metadata keys.
     """
-    cfg = recreate_branches(cfg)
-
-    del_keys = []
-    for key in cfg.keys():
-        if is_meta_key(key):
-            del_keys.append(key)
-        elif isinstance(cfg[key], dict):
-            dic = cfg[key]
-            for dic_key in [k for k in dic.keys() if is_meta_key(k)]:
-                del dic[dic_key]
-
-    for key in del_keys:
-        del cfg[key]
-
+    if cfg:
+        cfg = recreate_branches(cfg, skip_keys=meta_keys)
     return cfg
 
 
-def recreate_branches(data):
+def recreate_branches(data, skip_keys=None):
     new_data = data
     if isinstance(data, (Namespace, dict)):
         new_data = type(data)()
         for key, val in getattr(data, '__dict__', data).items():
-            new_data[key] = recreate_branches(val)
+            if skip_keys is None or key not in skip_keys:
+                new_data[key] = recreate_branches(val, skip_keys)
     elif isinstance(data, list):
-        new_data = [recreate_branches(v) for v in data]
+        new_data = [recreate_branches(v, skip_keys) for v in data]
     return new_data
 
 
@@ -239,23 +226,25 @@ class Namespace(argparse.Namespace):
             setattr(flat, key, val)
         return flat
 
-    def items(self) -> Iterator[Tuple[str, Any]]:
-        """Returns a generator of all nested (key, value) items."""
+    def items(self, branches: bool = False) -> Iterator[Tuple[str, Any]]:
+        """Returns a generator of all leaf (key, value) items, optionally including branches."""
         for key, val in vars(self).items():
             if isinstance(val, Namespace):
+                if branches:
+                    yield key, val
                 for subkey, subval in val.items():
                     yield key+'.'+subkey, subval
             else:
                 yield key, val
 
-    def keys(self) -> Iterator[str]:
-        """Returns a generator of all nested keys."""
-        for key, _ in self.items():
+    def keys(self, branches: bool = False) -> Iterator[str]:
+        """Returns a generator of all leaf keys, optionally including branches."""
+        for key, _ in self.items(branches):
             yield key
 
-    def values(self) -> Iterator[Any]:
-        """Returns a generator of all nested values."""
-        for _, val in self.items():
+    def values(self, branches: bool = False) -> Iterator[Any]:
+        """Returns a generator of all leaf values, optionally including branches."""
+        for _, val in self.items(branches):
             yield val
 
     def get_sorted_keys(self, branches: bool = True, key_filter: Callable = is_meta_key) -> List[str]:
diff --git a/jsonargparse/optionals.py b/jsonargparse/optionals.py
index 98f1494..433628a 100644
--- a/jsonargparse/optionals.py
+++ b/jsonargparse/optionals.py
@@ -8,7 +8,6 @@ from contextlib import contextmanager
 from importlib.util import find_spec
 from typing import Optional
 
-from .namespace import Namespace
 
 __all__ = [
     'get_config_read_mode',
@@ -42,8 +41,15 @@ def typing_extensions_import(name):
         return getattr(typing, name, False)
 
 
+def is_compatible_final(final) -> bool:
+    @final
+    class FinalClass:
+        pass
+    return getattr(FinalClass, '__final__', False)
+
+
 final = typing_extensions_import('final')
-if not final or not getattr(final(Namespace()), '__final__', False) or 'SPHINX_BUILD' in os.environ:
+if not final or not is_compatible_final(final) or 'SPHINX_BUILD' in os.environ:
     def final(cls):  # pylint: disable=function-redefined
         """Decorator to make a class ``final``, i.e., it shouldn't be subclassed.
 
@@ -249,7 +255,7 @@ def argcomplete_autocomplete(parser):
 
 def argcomplete_namespace(caller, parser, namespace):
     if caller == 'argcomplete':
-        namespace.__class__ = Namespace
+        namespace.__class__ = __import__('jsonargparse').Namespace
         namespace = parser.merge_config(parser.get_defaults(skip_check=True), namespace).as_flat()
     return namespace
 
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index 9ec3271..0f87f47 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -284,12 +284,17 @@ class ActionTypeHint(Action):
 
 
     @staticmethod
-    def discard_init_args_on_class_path_change(parser, cfg_to, cfg_from):
-        for action in [a for a in parser._actions if ActionTypeHint.is_subclass_typehint(a, all_subtypes=False)]:
-            val_to = cfg_to.get(action.dest)
-            val_from = cfg_from.get(action.dest)
-            if is_subclass_spec(val_to) and is_subclass_spec(val_from):
-                discard_init_args_on_class_path_change(action, val_to, val_from)
+    def discard_init_args_on_class_path_change(parser_or_action, prev_cfg, cfg):
+        keys = list(prev_cfg.keys(branches=True))
+        for key in keys:
+            prev_val = prev_cfg.get(key)
+            val = cfg.get(key)
+            if is_subclass_spec(prev_val) and is_subclass_spec(val):
+                action = parser_or_action
+                if not isinstance(parser_or_action, ActionTypeHint):
+                    action = _find_action(parser_or_action, key)
+                if isinstance(action, ActionTypeHint):
+                    discard_init_args_on_class_path_change(action, prev_val, val)
 
 
     @staticmethod
@@ -365,6 +370,14 @@ class ActionTypeHint(Action):
                 val = NestedArg(key=sub_opt, val=val)
             append = opt_str == f'--{self.dest}+'
             val = self._check_type(val, append=append, cfg=cfg)
+            if is_subclass_spec(val):
+                prev_val = cfg.get(self.dest)
+                if is_subclass_spec(prev_val) and 'init_args' in prev_val:
+                    ActionTypeHint.discard_init_args_on_class_path_change(
+                        self,
+                        prev_val.init_args,
+                        val.get('init_args'),
+                    )
         cfg.update(val, self.dest)
 
 
@@ -402,7 +415,7 @@ class ActionTypeHint(Action):
                             ex = None
                     except ValueError:
                         if self._enable_path and config_path is None and isinstance(orig_val, str):
-                            msg = f'\n- Expected a config path but "{orig_val}" either not accessible or invalid.\n- '
+                            msg = f'\n- Expected a config path but {orig_val} either not accessible or invalid\n- '
                             raise type(ex)(msg+str(ex)) from ex
                     if ex:
                         raise ex
@@ -419,7 +432,8 @@ class ActionTypeHint(Action):
                 value[num] = val
             except (TypeError, ValueError) as ex:
                 elem = '' if not islist else f' element {num+1}'
-                raise TypeError(f'Parser key "{self.dest}"{elem}: {ex}') from ex
+                error = indent_text(str(ex))
+                raise TypeError(f'Parser key "{self.dest}"{elem}: {error}') from ex
         return value if islist else value[0]
 
 
@@ -624,8 +638,9 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
                     prev_val = [adapt_typehints(prev_val, subtypehints[0], **adapt_kwargs)]
                 except Exception:
                     prev_val = []
-            val = prev_val + (val if isinstance(val, list) else [adapt_typehints(val, subtypehints[0], **adapt_kwargs)])
-            prev_val = prev_val + [None] * (len(val) if isinstance(val, list) else 1)
+            val_is_list = isinstance(val, list)
+            val = prev_val + (val if val_is_list else [val])
+            prev_val = prev_val + [None] * (len(val)-len(prev_val) if val_is_list else 1)
         if isinstance(val, NestedArg) and subtypehints is not None:
             val = (prev_val[:-1] if isinstance(prev_val, list) else []) + [val]
         elif not isinstance(val, list):
@@ -708,9 +723,11 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
         val = subclass_spec_as_namespace(val, prev_val)
         if not is_subclass_spec(val):
             raise_unexpected_value(
-                f'Subclass type {typehint.__name__} expects one of:\n  - a class path (str)\n  - a dict with '
-                f'class_path entry\n  - a dict without class_path but with init_args entry (class path given previously)',
-                val_input,
+                f'Not a valid {typehint.__name__}. Got value: {val_input}\n'
+                'Subclass types expect one of:\n'
+                '- a class path (str)\n'
+                '- a dict with class_path entry\n'
+                '- a dict without class_path but with init_args entry (class path given previously)'
             )
 
         try:


Test patch:
diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py
index eca8738..9932d19 100755
--- a/jsonargparse_tests/test_signatures.py
+++ b/jsonargparse_tests/test_signatures.py
@@ -505,6 +505,38 @@ class SignaturesTests(unittest.TestCase):
             self.assertIn("discarding init_args: {'pa': 'A', 'pc': 'X'}", str(w[0].message))
 
 
+    def test_add_subclass_nested_discard_init_args(self):
+        class ChildBase:
+            pass
+
+        class A(ChildBase):
+            def __init__(self, a: int = 0):
+                pass
+
+        class B(ChildBase):
+            def __init__(self, b: int = 0):
+                pass
+
+        class Parent:
+            def __init__(self, c: ChildBase):
+                pass
+
+        with mock_module(Parent, ChildBase, A, B) as module, warnings.catch_warnings(record=True) as w:
+            parser = ArgumentParser(error_handler=None)
+            parser.add_subclass_arguments(Parent, 'p')
+            cfg = parser.parse_args([
+                '--p=Parent',
+                '--p.init_args.c=A',
+                '--p.init_args.c.init_args.a=1',
+                '--p.init_args.c=B',
+                '--p.init_args.c.init_args.b=2',
+            ])
+            self.assertEqual(cfg.p.class_path, f'{module}.Parent')
+            self.assertEqual(cfg.p.init_args.c.class_path, f'{module}.B')
+            self.assertEqual(cfg.p.init_args.c.init_args, Namespace(b=2))
+            self.assertIn("discarding init_args: {'a': 1}", str(w[0].message))
+
+
     def test_class_path_override_with_mixed_type(self):
         class MyCalendar(Calendar):
             def __init__(self, *args, param: int = 0, **kwargs):