Repository: omni-us/jsonargparse

Issue:
link_arguments target not ignored when it is deeply nested and its parent is undefined
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

## 🐛 Bug report

First of all, thanks for the great tool! `jonsargparse` has helped us standardizing cli usages and configuration management extremely well!
According to https://jsonargparse.readthedocs.io/en/v4.36.0/#argument-linking, if a subclass does not define the targeted init_args parameter, the link is ignored. However it fails on a deeply nested target when its parent is undefined. For example, the target `trainer.strategy.init_args.nested_arg.init_args.foo` causes error in arg parsing when `nested_arg` is not defined. While the target `trainer.strategy.init_args.arg` won't cause error even if `arg` isn't defined.

### To reproduce

```python
import jsonargparse

class Strategy:
    pass

class StrategyA(Strategy):
    pass

class StrategyB(Strategy):
    def __init__(self, arg: str, nested_arg: "NestedArg"):
        self.arg = arg
        self.nested_arg = nested_arg

class NestedArg:
    def __init__(self, foo: str):
        self.foo = foo

class Trainer:
    def __init__(self, strategy: Strategy | None = None):
        self.strategy = strategy


if __name__ == "__main__":
    parser = jsonargparse.ArgumentParser()
    parser.add_class_arguments(Trainer, "trainer")
    parser.add_argument("--arg", type=str)

    trainer_strategy_a = {
        "strategy": {
            "class_path": "__main__.StrategyA",
        }
    }
    trainer_strategy_b = {
        "strategy": {
            "class_path": "__main__.StrategyB",
            "init_args": {"nested_arg": "NestedArg"}
        }
    }
    # target ignored without errors
    parser.link_arguments("arg", "trainer.strategy.init_args.arg")
    cfg = parser.parse_args(["--arg", "bar", "--trainer", str(trainer_strategy_a)])
    parser.instantiate_classes(cfg)
    # target successfully populated
    parser.link_arguments("arg", "trainer.strategy.init_args.nested_arg.init_args.foo")
    cfg = parser.parse_args(["--arg", "bar", "--trainer", str(trainer_strategy_b)])
    parser.instantiate_classes(cfg)
    # target cause errors
    cfg = parser.parse_args(["--arg", "bar", "--trainer", str(trainer_strategy_a)])
    parser.instantiate_classes(cfg)
```
#### Output
```
Traceback (most recent call last):
  File "/home/user/Projects/.local/link_arg.py", line 48, in <module>
    cfg = parser.parse_args(["--arg", "bar", "--trainer", str(trainer_value)])
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_deprecated.py", line 123, in patched_parse
    cfg = parse_method(*args, _skip_validation=_skip_validation, **kwargs)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_core.py", line 430, in parse_args
    cfg, unk = self.parse_known_args(args=args, namespace=cfg)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_core.py", line 298, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace, **kwargs)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/argparse.py", line 2079, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/argparse.py", line 2019, in consume_optional
    take_action(action, args, option_string)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/argparse.py", line 1943, in take_action
    action(self, namespace, argument_values, option_string)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_actions.py", line 317, in __call__
    loaded_value = self._load_config(value, parser)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_actions.py", line 331, in _load_config
    cfg = parser._apply_actions(cfg, parent_key=self.dest)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_core.py", line 1344, in _apply_actions
    value = self._check_value_key(action, value, action_dest, prev_cfg)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_core.py", line 1396, in _check_value_key
    value = action._check_type_(value, cfg=cfg)  # type: ignore[attr-defined]
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_common.py", line 275, in _check_type_
    return self._check_type(value, **kwargs)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_typehints.py", line 582, in _check_type
    val = adapt_typehints(val, self._typehint, **kwargs)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_typehints.py", line 835, in adapt_typehints
    raise_union_unexpected_value(sorted_subtypes, val, vals)
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_typehints.py", line 711, in raise_union_unexpected_value
    str_exceptions = [indent_text(str(e), first_line=False) for e in exceptions]
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_typehints.py", line 711, in <listcomp>
    str_exceptions = [indent_text(str(e), first_line=False) for e in exceptions]
  File "/home/user/miniconda3/envs/finetune/lib/python3.10/site-packages/jsonargparse/_util.py", line 350, in indent_text
    return lines[0] + os.linesep + textwrap.indent(os.linesep.join(lines[1:]), "  ")
IndexError: list index out of range
```

### Expected behavior

The snippet above should run without errors and the classes are instantiated correctly.

### Environment

<!-- Fill in the list below. -->

- jsonargparse version: 4.36.0
- Python version: 3.10.16
- How jsonargparse was installed: pip install
- OS: Linux

Discussion:
@mauvilsa: Thank you for reporting! Would you be interested in contributing the fix?

Solution patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index de173b2b..76fb375b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -27,6 +27,8 @@ Fixed
   <https://github.com/omni-us/jsonargparse/pull/662>`__).
 - Pydantic discriminated unions handled incorrectly (`#667
   <https://github.com/omni-us/jsonargparse/pull/667>`__).
+- Failure when a link target has an undefined parent (`#668
+  <https://github.com/omni-us/jsonargparse/pull/668>`__)
 
 
 v4.36.0 (2025-01-17)
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index b87b42cc..b3c72bf5 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -1374,7 +1374,10 @@ def adapt_class_type(
 
             parent_key, key = target.split(split, maxsplit=1)
 
-            action = next(a for a in parser._actions if a.dest == parent_key)
+            try:
+                action = next(a for a in parser._actions if a.dest == parent_key)
+            except StopIteration:
+                continue
 
             sub_add_kwargs = getattr(action, "sub_add_kwargs")
             sub_add_kwargs.setdefault("linked_targets", set())


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index fbbd1707..c4e076eb 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -1139,6 +1139,27 @@ def test_on_instantiate_linking_deep_targets_mapping(parser, tmp_path):
     assert isinstance(config_init["b"].a_map["name"].d, DeepD)
 
 
+def test_on_instantiate_linking_deep_targets_undefined_parent(parser, tmp_path):
+    config = {
+        "b": {
+            "class_path": f"{__name__}.DeepBSuper",
+        },
+        "c": {},
+    }
+    config_path = tmp_path / "config.yaml"
+    config_path.write_text(json_or_yaml_dump(config))
+
+    parser.add_argument("--config", action="config")
+    parser.add_subclass_arguments(DeepBSuper, nested_key="b", required=True)
+    parser.add_class_arguments(DeepC, nested_key="c")
+    # .init_args.a is undefined in DeepBSuper
+    parser.link_arguments("c", "b.init_args.a.init_args.d", compute_fn=DeepC.fn, apply_on="instantiate")
+
+    config = parser.parse_args([f"--config={config_path}"])
+    config_init = parser.instantiate_classes(config)
+    assert isinstance(config_init["b"], DeepBSuper)
+
+
 class DeepTarget:
     def __init__(self, a: int, b: int) -> None:
         self.a = a