Repository: omni-us/jsonargparse

Issue:
Unable to specify a path for a callable parameter of a class
## 🐛 Bug report

When define a parameter of a class to be callable type, it is unable to specify a path for this parameter.

### To reproduce
```python
from collections.abc import Callable

from jsonargparse import ArgumentParser

class B:
    def __init__(self, x: int):
        self.x = x

class A:
    def __init__(self, b: Callable[[int], B]):
        self.b = b(id(self))

def main():
    parser = ArgumentParser()
    parser.add_class_arguments(A, 'a', sub_configs=True)
    '''
        content of b.yaml:
        class_path: B
    '''
    args = parser.parse_args(['--a.b', 'b.yaml'])

if __name__ == '__main__':
    main()

```

### Expected behavior

<!-- Describe how the behavior or output of the reproduction snippet should be. -->

### Actual output

```plain
usage: test.py [-h] [--a CONFIG] --a.b B
error: Parser key "a.b":
  Type collections.abc.Callable[[int], __main__.B] expects a function or a callable class: No module named 'b'. Got value: b.yaml
```

### Environment

<!-- Fill in the list below. -->

- jsonargparse version: 4.27.0
- Python version: 3.11.6
- How jsonargparse was installed: `jsonargparse[signatures, omegaconf]`
- OS: 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=424)

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



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4b32e06..9482571 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,8 @@ Fixed
 - Confusing error message when adding signature parameters that conflict with
   existing arguments.
 - Deprecation warnings not printing the correct file and line of code.
+- ``sub_configs=True`` not working for callable types that return a class (`#419
+  <https://github.com/omni-us/jsonargparse/issues/419>`__).
 
 
 v4.27.0 (2023-11-02)
diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py
index 1cc27ff..cc1aa46 100644
--- a/jsonargparse/_signatures.py
+++ b/jsonargparse/_signatures.py
@@ -244,6 +244,8 @@ class SignatureArguments(LoggerProperty):
 
         prefix = "--" + (nested_key + "." if nested_key else "")
         for param in params:
+            if skip and param.name in skip:
+                continue
             if prefix + param.name in self._option_string_actions:  # type: ignore
                 raise ValueError(
                     f"Unable to add parameter '{param.name}' from {function_or_class} because "
@@ -363,10 +365,13 @@ class SignatureArguments(LoggerProperty):
                     sub_add_kwargs["skip"] = subclass_skip
                 else:
                     register_pydantic_type(annotation)
+                enable_path = sub_configs and (
+                    is_subclass_typehint or ActionTypeHint.is_return_subclass_typehint(annotation)
+                )
                 args = ActionTypeHint.prepare_add_argument(
                     args=args,
                     kwargs=kwargs,
-                    enable_path=is_subclass_typehint and sub_configs,
+                    enable_path=enable_path,
                     container=group,
                     logger=self.logger,
                     sub_add_kwargs=sub_add_kwargs,
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index 3042de3..4cdb285 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -278,6 +278,15 @@ class ActionTypeHint(Action):
             and not is_subclass(typehint, (Path, Enum))
         )
 
+    @staticmethod
+    def is_return_subclass_typehint(typehint):
+        typehint_origin = get_typehint_origin(typehint)
+        if typehint_origin in callable_origin_types:
+            return_type = get_callable_return_type(typehint)
+            if ActionTypeHint.is_subclass_typehint(return_type):
+                return True
+        return False
+
     @staticmethod
     def is_mapping_typehint(typehint):
         typehint_origin = get_typehint_origin(typehint) or typehint


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 00a5206..fd714a0 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -6,6 +6,7 @@ import time
 import uuid
 from calendar import Calendar
 from enum import Enum
+from pathlib import Path
 from typing import (
     Any,
     Callable,
@@ -784,6 +785,27 @@ def test_callable_args_return_type_union_of_classes(parser, subtests):
             assert f"{__name__}.{name}" in help_str
 
 
+class CallableSubconfig:
+    def __init__(self, o: Callable[[int], Optimizer]):
+        self.o = o
+
+
+def test_callable_args_return_type_class_subconfig(parser, tmp_cwd):
+    config = {
+        "class_path": "Adam",
+        "init_args": {"momentum": 0.8},
+    }
+    Path("optimizer.yaml").write_text(yaml.safe_dump(config))
+
+    parser.add_class_arguments(CallableSubconfig, "m", sub_configs=True)
+    cfg = parser.parse_args(["--m.o=optimizer.yaml"])
+    assert cfg.m.o.class_path == f"{__name__}.Adam"
+    init = parser.instantiate_classes(cfg)
+    optimizer = init.m.o(1)
+    assert isinstance(optimizer, Adam)
+    assert optimizer.momentum == 0.8
+
+
 # lazy_instance tests