Repository: omni-us/jsonargparse

Issue:
Link arguments "Target key must be for an individual argument"
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

## 🐛 Bug report

Certain argument links produce an error. For example, using a subclass of the `LightningCLI` that links the trainer `default_root_dir` to the logger `save_dir` arguments, the following error occurs.

```bash
Traceback (most recent call last):
...
  File ".../clitest.py", line 8, in add_arguments_to_parser
    parser.link_arguments("trainer.default_root_dir", "trainer.logger.init_args.save_dir")
  File ".../lib/python3.10/site-packages/jsonargparse/link_arguments.py", line 379, in link_arguments
    ActionLink(self, source, target, compute_fn, apply_on)
  File ".../lib/python3.10/site-packages/jsonargparse/link_arguments.py", line 141, in __init__
    raise ValueError(f'Target key "{target}" must be for an individual argument.')
ValueError: Target key "trainer.logger.init_args.save_dir" must be for an individual argument.
```

A link in the the other direction `save_dir` -> `default_root_dir` seems to work okay,
I suspect the issue may be to do with the fact that the target key exists within an optional `trainer.logger` class.

### To reproduce

```python
from pytorch_lightning.cli import LightningCLI
from pytorch_lightning.demos.boring_classes import BoringDataModule, DemoModel


class MyCLI(LightningCLI):
    def add_arguments_to_parser(self, parser) -> None:
        #parser.link_arguments("trainer.logger.init_args.save_dir", "trainer.default_root_dir") # works 
        parser.link_arguments("trainer.default_root_dir", "trainer.logger.init_args.save_dir") # error


def cli_main():
    cli = MyCLI(DemoModel, BoringDataModule)


if __name__ == "__main__":
    cli_main()
```

### Expected behavior


I'm opening an issue here as:
- It would be nice to allow the link to be ignored when the target is null (perhaps with an additional parameter to the `link_arguments` function e.g. `ignore_null_target`)
- In any case, the error message could be improved.



### Environment

<!-- Fill in the list below. -->
- jsonargparse 4.18.0
- Python 3.10.6

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/223?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 [(`392c6b8`)](https://codecov.io/gh/omni-us/jsonargparse/pull/223?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) compared to base [(`0d73a84`)](https://codecov.io/gh/omni-us/jsonargparse/commit/0d73a844ea6355dfddc49c145e25e7d4f22ffe83?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      #223   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           19        19           
  Lines         5074      5074           
=========================================
  Hits          5074      5074           
```

| Flag | Coverage Δ | |
|---|---|---|
| py3.10 | `87.62% <100.00%> (ø)` | |
| py3.10_all | `99.88% <100.00%> (ø)` | |
| py3.6 | `87.27% <100.00%> (ø)` | |
| py3.6_all | `99.62% <100.00%> (ø)` | |
| py3.7 | `87.29% <100.00%> (ø)` | |
| py3.7_all | `99.80% <100.00%> (ø)` | |
| py3.8 | `87.60% <100.00%> (ø)` | |
| py3.8_all | `99.84% <100.00%> (ø)` | |
| py3.9 | `87.60% <100.00%> (ø)` | |
| py3.9_all | `99.86% <100.00%> (ø)` | |

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/223?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/link\_arguments.py](https://codecov.io/gh/omni-us/jsonargparse/pull/223/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL2xpbmtfYXJndW1lbnRzLnB5) | `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/223?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=223)

[![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=223&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=223&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=223&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=223&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=223&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=223&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=223&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=223&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=223&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=223&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=223&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=223&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=223) 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=223&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=223&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6a9373a..688ae1c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -43,6 +43,8 @@ Fixed
 - ``fail_untyped=False`` not propagated to subclass ``--*.help`` actions.
 - Issues reported by CodeQL.
 - Incorrect value when ``Path`` is cast to ``str`` and ``rel_path`` was changed.
+- Argument links with target a subclass mixed with other types not working `#208
+  <https://github.com/omni-us/jsonargparse/issues/208>`__.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/link_arguments.py b/jsonargparse/link_arguments.py
index 0329aab..97030b3 100644
--- a/jsonargparse/link_arguments.py
+++ b/jsonargparse/link_arguments.py
@@ -135,7 +135,7 @@ class ActionLink(Action):
         assert self.target[1] is not None
 
         from .typehints import ActionTypeHint
-        is_target_subclass = ActionTypeHint.is_subclass_typehint(self.target[1])
+        is_target_subclass = ActionTypeHint.is_subclass_typehint(self.target[1], all_subtypes=False)
         valid_target_init_arg = is_target_subclass and target.startswith(self.target[1].dest+'.init_args.')
         valid_target_leaf = self.target[1].dest == target
         if not valid_target_leaf and is_target_subclass and not valid_target_init_arg:
@@ -321,7 +321,7 @@ class ActionLink(Action):
     def set_target_value(action: 'ActionLink', value: Any, cfg: Namespace, logger) -> None:
         target_key, target_action = action.target
         from .typehints import ActionTypeHint
-        if ActionTypeHint.is_subclass_typehint(target_action):
+        if ActionTypeHint.is_subclass_typehint(target_action, all_subtypes=False):
             if target_key == target_action.dest:  # type: ignore
                 target_action._check_type(value)  # type: ignore
             elif target_key not in cfg:


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index 1c69b25..eae5f32 100755
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -183,6 +183,30 @@ class LinkArgumentsTests(unittest.TestCase):
                 parser.parse_args(['--a='+json.dumps(a_value), '--c=calendar.Calendar'])
 
 
+    def test_link_arguments_on_parse_mixed_subclass_target(self):
+        class Logger:
+            def __init__(self, save_dir: Optional[str] = None):
+                pass
+
+        class Trainer:
+            def __init__(
+                self,
+                save_dir: Optional[str] = None,
+                logger: Union[bool, Logger] = False,
+            ):
+                pass
+
+        with mock_module(Logger):
+            parser = ArgumentParser()
+            parser.add_class_arguments(Trainer, 'trainer')
+            parser.link_arguments('trainer.save_dir', 'trainer.logger.init_args.save_dir')
+            cfg = parser.parse_args([])
+            self.assertEqual(cfg.trainer, Namespace(logger=False, save_dir=None))
+            cfg = parser.parse_args(['--trainer.save_dir=logs', '--trainer.logger=Logger'])
+            self.assertEqual(cfg.trainer.save_dir, 'logs')
+            self.assertEqual(cfg.trainer.logger.init_args, Namespace(save_dir='logs'))
+
+
     def test_link_arguments_on_parse_add_subclass_arguments_with_instantiate_false(self):
         class ClassA:
             def __init__(