Repository: omni-us/jsonargparse

Issue:
Issue with argument linking to list of subclass objects
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

I started using the (very cool) feature of linking an argument to a list of subclass objects, as shown in the [documentation](https://jsonargparse.readthedocs.io/en/v4.27.2/#applied-on-parse).

It seems that when the target argument is a list of ParentClass defined as a class property, I cannot set it simply as `List[ParentClass]`. If I do so, I get an exception. In order for the link to work as expected, I must define the argument as a union of both ParentClass and a list of ParentClass: `Union[ParentClass, List[ParentClass]]`

In my use case, a single object rather than a list doesn't make sense for the relevant class property, so I would prefer to only allow for the property to be a list.

Interestingly, this problem doesn't happen if I define the argument directly using parser.add_argument, I only get it when I use parser.add_class_arugments.

```python
from typing import List, Union

from jsonargparse import ArgumentParser, dict_to_namespace


class MyClass(object):
    def __init__(self, simple_str: str = '', an_int: int = 0):
        self.simple_str = simple_str
        self.an_int = an_int

    def __repr__(self):
        return f'{self.simple_str}, {self.an_int}'


class MySubClass(MyClass):

    def __init__(self, str_list: List[str] = None, **kwargs):
        if str_list is None:
            str_list = []

        self.str_list = str_list
        super().__init__(**kwargs)

    def __repr__(self):
        return super().__repr__() + f', {self.str_list}'


class MainClass(object):
    def __init__(
            self,
            simple_str: str = '',
            str_list: List[str] = None,
            # my_class_list: List[MyClass] = None, # THIS GIVES ERROR
            my_class_list: Union[List[MyClass], MyClass] = None, # THIS WORKS FINE
    ):

        if str_list is None:
            str_list = []
        if my_class_list is None:
            my_class_list = []

        self.simple_str = simple_str
        self.str_list = str_list
        self.my_class_list = my_class_list


if __name__ == '__main__':
    parser = ArgumentParser(exit_on_error=False)
    parser.add_class_arguments(MainClass)

    parser.link_arguments('simple_str', 'my_class_list.init_args.simple_str')
    parser.link_arguments('str_list', 'my_class_list.init_args.str_list')
    value = {
        "simple_str": "a simple string",
        "str_list": ["a", "b", "c"],
        "my_class_list": [
            {
                "class_path": "MyClass",
                "init_args": {

                }
            },
            {
                "class_path": "MySubClass",
                "init_args": {
                    "an_int": 1,
                }
            }
        ]
    }

    cfg = parser.parse_args(args=[], namespace=dict_to_namespace(value))
    init = parser.instantiate_classes(cfg)
    print(init.my_class_list)

```

### Expected behavior

The argument linking should work when List[ParentClass] is defined as a property of a class.

### Environment

<!-- Fill in the list below. -->

- jsonargparse version (e.g., 4.8.0): 4.27.2
- Python version (e.g., 3.9): 3.8.5
- How jsonargparse was installed (e.g. `pip install jsonargparse[all]`): as part of pytorch lightning
- OS (e.g., Linux): Windows 11


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=434) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=434&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=434&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=434&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=434)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`a9fc1cd`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/a9fc1cd60393450eb0a5dd01a25cf29bf3784073?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`371265b`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00%.


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


```diff
@@            Coverage Diff            @@
##              main      #434   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5957      5958    +1     
=========================================
+ Hits          5957      5958    +1     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.30% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.72% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.01% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.72% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.87% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.49% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `89.02% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.49% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.87% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.37% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.37% <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.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/434?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](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).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ca42753..8eb3936 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
 paths are considered internals and can change in minor and patch releases.
 
 
+v4.27.3 (2024-01-??)
+--------------------
+
+Fixed
+^^^^^
+- Argument links not working for target ``init_args`` in an optional list (`#433
+  <https://github.com/omni-us/jsonargparse/issues/433>`__).
+
+
 v4.27.2 (2024-01-18)
 --------------------
 
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index a693535..740ac3d 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -267,7 +267,8 @@ class ActionTypeHint(Action):
         if typehint_origin == Union or (also_lists and typehint_origin in sequence_origin_types):
             subtypes = [a for a in typehint.__args__ if a != NoneType]
             test = all if all_subtypes else any
-            return test(ActionTypeHint.is_subclass_typehint(s) for s in subtypes)
+            k = {"also_lists": also_lists}
+            return test(ActionTypeHint.is_subclass_typehint(s, **k) for s in subtypes)
         return (
             inspect.isclass(typehint)
             and typehint not in leaf_or_root_types


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index 83c71c2..bdbe72d 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -281,6 +281,28 @@ def test_on_parse_subclass_target_in_union_list(parser):
     assert all(x.init_args == Namespace(save_dir="logs") for x in cfg.trainer.logger)
 
 
+class TrainerLoggerOptionalList:
+    def __init__(
+        self,
+        save_dir: Optional[str] = None,
+        logger: Optional[List[Logger]] = None,
+    ):
+        pass
+
+
+def test_on_parse_subclass_target_in_optional_list(parser):
+    parser.add_class_arguments(TrainerLoggerOptionalList, "trainer")
+    parser.link_arguments("trainer.save_dir", "trainer.logger.init_args.save_dir")
+    cfg = parser.parse_args([])
+    assert cfg.trainer == Namespace(logger=None, save_dir=None)
+    cfg = parser.parse_args(["--trainer.save_dir=logs", "--trainer.logger+=Logger"])
+    assert cfg.trainer.save_dir == "logs"
+    assert cfg.trainer.logger[0].init_args == Namespace(save_dir="logs")
+    cfg = parser.parse_args(["--trainer.save_dir=logs", "--trainer.logger=[Logger, Logger]"])
+    assert len(cfg.trainer.logger) == 2
+    assert all(x.init_args == Namespace(save_dir="logs") for x in cfg.trainer.logger)
+
+
 class ClassF:
     def __init__(
         self,