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=175)

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



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 65a3d33..cba0a11 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,6 +24,8 @@ Fixed
 - ``default_env`` not forwarded to subcommand parsers, causing environment
   variable names to not be shown in subcommand help `pytorch-lightning#12790
   <https://github.com/Lightning-AI/lightning/issues/12790>`__.
+- Cannot override Callable ``init_args`` without passing the ``class_path``
+  `#174 <https://github.com/omni-us/jsonargparse/issues/174>`__.
 
 
 v4.15.1 (2022-10-07)
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index 6f48e89..b163825 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -234,6 +234,12 @@ class ActionTypeHint(Action):
         return False
 
 
+    @staticmethod
+    def is_callable_typehint(typehint):
+        typehint_origin = get_typehint_origin(typehint)
+        return typehint_origin in callable_origin_types or typehint in callable_origin_types
+
+
     def is_init_arg_mapping_typehint(self, key, cfg):
         result = False
         class_path = cfg.get(f'{self.dest}.class_path')
@@ -259,8 +265,9 @@ class ActionTypeHint(Action):
                 action = _find_parent_action(parser, arg_base[2:])
 
         typehint = typehint_from_action(action)
-        if (
+        if typehint and (
             ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False) or
+            ActionTypeHint.is_callable_typehint(typehint) or
             ActionTypeHint.is_mapping_typehint(typehint)
         ):
             return action, arg_base, explicit_arg
@@ -629,14 +636,16 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
                 if isinstance(val, str):
                     val_obj = import_object(val)
                     if inspect.isclass(val_obj):
-                        val = {'class_path': val}
+                        val = Namespace(class_path=val)
                     elif callable(val_obj):
                         val = val_obj
                     else:
                         raise ImportError(f'Unexpected import object {val_obj}')
-                if isinstance(val, (dict, Namespace)):
+                if isinstance(val, (dict, Namespace, NestedArg)):
+                    val_input = val
+                    val = subclass_spec_as_namespace(val, prev_val)
                     if not is_subclass_spec(val):
-                        raise ImportError(f'Dict must include a class_path and optionally init_args, but got {val}')
+                        raise ImportError(f'Dict must include a class_path and optionally init_args, but got {val_input}')
                     val_class = import_object(val['class_path'])
                     if not (inspect.isclass(val_class) and callable_instances(val_class)):
                         raise ImportError(f'{val["class_path"]!r} is not a callable class.')


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 8185f2c..af135f0 100755
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -479,6 +479,24 @@ class TypeHintsTests(unittest.TestCase):
             self.assertRaises(ParserError, lambda: parser.parse_args([f'--callable={json.dumps(value)}']))
 
 
+    def test_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=Callable)
+
+        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)