Repository: omni-us/jsonargparse

Issue:
Cannot override Callable init_args without passing the class_path
## 🐛 Bug report

Unlike the class argument, I cannot override `Callable` argument without passing the class_path even it is given previously.

### To reproduce

```python
from jsonargparse import CLI
from typing import Callable

def call():
    print("Hi")

class Call:
    def __init__(self, name: str):
        self.name = name

    def __call__(self):
        print(f"Hi, {self.name}")

def fn(
    callable: Callable = call,
):
    callable()

if __name__ == "__main__":
    CLI(fn)
```

```yaml
callable:
  class_path: __main__.Call
  init_args:
    name: Alice
```

```console
python test.py --config test.yaml
Hi, Alice
python test.py --config test.yaml --callable.name Bob
usage: test.py [-h] [--config CONFIG] [--print_config [={comments,skip_null,skip_default}+]] [--callable CALLABLE]
test.py: error: Unrecognized arguments: --callable.name Bob
```

### Expected behavior

```console
python test.py --config test.yaml
Hi, Alice
python test.py --config test.yaml --callable.name Bob
Hi, Bob
```

### Environment

- jsonargparse version: 4.15.1
- Python version: 3.9
- How jsonargparse was installed: `pip install jsonargparse`
- OS (e.g., Linux): macOS & Linux


Discussion:
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=176)

[![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=176&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=176&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=176&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=176&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=176&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=176&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=176&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=176&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=176&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=176&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=176&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=176&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=176) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=176&metric=duplicated_lines_density&view=list) No Duplication information


codecov-commenter: # [Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/176?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 [(`44c5bae`)](https://codecov.io/gh/omni-us/jsonargparse/pull/176?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) compared to base [(`04383f9`)](https://codecov.io/gh/omni-us/jsonargparse/commit/04383f9d38d4096b7d149c8a4d2d57d21d521438?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      #176   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         4319      4323    +4     
=========================================
+ Hits          4319      4323    +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| py3.10_all | `99.93% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.6 | `90.71% <100.00%> (+0.01%)` | :arrow_up: |
| py3.6_all | `99.71% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.7_all | `99.83% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.8 | `90.97% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.8_all | `99.90% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.9_all | `99.90% <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/176?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/typehints.py](https://codecov.io/gh/omni-us/jsonargparse/pull/176/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/176?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).


Patch:
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index b163825..e4f1d2d 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -235,8 +235,12 @@ class ActionTypeHint(Action):
 
 
     @staticmethod
-    def is_callable_typehint(typehint):
+    def is_callable_typehint(typehint, all_subtypes=True):
         typehint_origin = get_typehint_origin(typehint)
+        if typehint_origin == Union:
+            subtypes = [a for a in typehint.__args__ if a != NoneType]
+            test = all if all_subtypes else any
+            return test(ActionTypeHint.is_callable_typehint(s) for s in subtypes)
         return typehint_origin in callable_origin_types or typehint in callable_origin_types
 
 
@@ -267,7 +271,7 @@ class ActionTypeHint(Action):
         typehint = typehint_from_action(action)
         if typehint and (
             ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False) or
-            ActionTypeHint.is_callable_typehint(typehint) or
+            ActionTypeHint.is_callable_typehint(typehint, all_subtypes=False) or
             ActionTypeHint.is_mapping_typehint(typehint)
         ):
             return action, arg_base, explicit_arg


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index af135f0..7d31c88 100755
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -497,6 +497,24 @@ class TypeHintsTests(unittest.TestCase):
             self.assertEqual(init.call(), 'Bob')
 
 
+    def test_union_callable_with_class_path_short_init_args(self):
+        class MyCallable:
+            def __init__(self, name: str):
+                self.name = name
+            def __call__(self):
+                return self.name
+
+        parser = ArgumentParser()
+        parser.add_argument('--call', type=Union[Callable, None])
+
+        with mock_module(MyCallable) as module:
+            cfg = parser.parse_args([f'--call={module}.MyCallable', '--call.name=Bob'])
+            self.assertEqual(cfg.call.class_path, f'{module}.MyCallable')
+            self.assertEqual(cfg.call.init_args, Namespace(name='Bob'))
+            init = parser.instantiate_classes(cfg)
+            self.assertEqual(init.call(), 'Bob')
+
+
     def test_typed_Callable_with_function_path(self):
         def my_func_1(p: int) -> str:
             return str(p)