Repository: omni-us/jsonargparse (python)

Issue:
class instantiation with multiple parameters
Hello, I have a question on how I can instantiate a class that requires multiple parameters that determined during run time only?
I followed the example on [Optimizer](https://jsonargparse.readthedocs.io/en/v4.24.0/#callable-type) but can't figure out how I can expand it to multiple parameters:

```python
# Example from the website
class Optimizer:
    def __init__(self, params: Iterable):
        self.params = params


class SGD(Optimizer):
    def __init__(self, params: Iterable, lr: float):
        super().__init__(params)
        self.lr = lr


if __name__ == '__main__':
    value = {
        "class_path": "SGD",
        "init_args": {
            "lr": 0.01,
        },
    }

    parser = ArgumentParser()
    parser.add_argument("--optimizer", type=Callable[[Iterable], Optimizer])
    cfg = parser.parse_args(["--optimizer", str(value)])
    init = parser.instantiate_classes(cfg)
    optimizer = init.optimizer([1, 2, 3])
```

Let's say I want to add one more parameter to `SGD(params, weight, lr)`. How will the flow change?

```python
class SGD(Optimizer):
    def __init__(self, params: Iterable, weight: float, lr: float):
...
parser.add_argument("--optimizer", type=Callable[[Iterable, float], Optimizer])
```
gives the following error:
```
usage: example.py [-h] [--optimizer OPTIMIZER]
example.py: error: Parser key "optimizer":
  Expected a dot import path string: SGD
```
Setting `class_path` to `__main__.SGD` produces another error:
```
usage: example.py [-h] [--optimizer OPTIMIZER]
example.py: error: Parser key "optimizer":
  Type typing.Callable[[typing.Iterable, float], __main__.Optimizer] expects a function or a callable class: Expected '__main__.SGD' to be a class that instantiates into callable or a subclass of False.. Got value: Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.01))

```

Pull request description:
fix `parse_args` for `Callable` with multiple input arguments
<!--
Thank you very much for contributing! If you like this project, please ⭐ star it
https://github.com/omni-us/jsonargparse/
-->

## What does this PR do?

Fixes #372.

<!--
Concisely describe what this pull request does. If available, include links to
issues web pages where the need for this change has been motivated.
-->

## Before submitting

<!--
Use the following list as tasks to be completed before marking a pull request as
"Ready for review". Fill with an "x" all tasks done. Use "n/a" if you think a
task is not relevant or leave empty when in doubt.
-->

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [x] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b3165af..94c24d4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -29,6 +29,8 @@ Fixed
 - ``--print_config`` fails when parser has shallow links.
 - Argument links unnecessarily applied when ``--print_config`` used and parser
   has subcommands (`#311 <https://github.com/omni-us/jsonargparse/issue/311>`__).
+- ``parse_args`` fails to parse arguments when data type is a ``Callable`` with multiple input arguments
+  (`#372 <https://github.com/omni-us/jsonargparse/issues/372>`__).
 - Postponed annotations not working for dataclasses.
 
 Changed
diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst
index 7363c17..971b9d6 100644
--- a/DOCUMENTATION.rst
+++ b/DOCUMENTATION.rst
@@ -1018,6 +1018,13 @@ A possible parser and callable behavior would be:
     >>> optimizer.params, optimizer.lr
     ([1, 2, 3], 0.01)
 
+Multiple parameters available after injection are also supported and can be
+specified the same way with a ``Callable`` type hint. For example, for two
+``Iterable`` parameters, you can use the following syntax:
+``Callable[[Iterable, Iterable], Type]``. Please be aware that the parameters
+are passed as positional arguments, this means that the injected function would
+be called like ``function(value1, value2)``.
+
 .. note::
 
     When the ``Callable`` has a class return type, it is possible to specify the
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index 31ebc99..22ece14 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -887,7 +887,7 @@ def subclass_spec_as_namespace(val, prev_val=None):
 def get_callable_return_type(typehint):
     return_type = None
     if len(getattr(typehint, "__args__", None) or []) > 1:
-        return_type = typehint.__args__[1]
+        return_type = typehint.__args__[-1]
     return return_type
 
 


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index d0cb136..00a5206 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -640,9 +640,10 @@ def test_callable_args_function_path(parser):
 
 
 class Optimizer:
-    def __init__(self, params: List[float], lr: float = 1e-3):
+    def __init__(self, params: List[float], lr: float = 1e-3, momentum: float = 0.0):
         self.params = params
         self.lr = lr
+        self.momentum = momentum
 
 
 class SGD(Optimizer):
@@ -665,6 +666,7 @@ def test_callable_args_return_type_class(parser, subtests):
         assert isinstance(optimizer, SGD)
         assert [0.1, 2, 3] == optimizer.params
         assert 1e-3 == optimizer.lr
+        assert 0.0 == optimizer.momentum
 
     with subtests.test("parse dict"):
         value = {
@@ -675,12 +677,13 @@ def test_callable_args_return_type_class(parser, subtests):
         }
         cfg = parser.parse_args([f"--optimizer={value}"])
         assert f"{__name__}.Adam" == cfg.optimizer.class_path
-        assert Namespace(lr=0.01) == cfg.optimizer.init_args
+        assert Namespace(lr=0.01, momentum=0.0) == cfg.optimizer.init_args
         init = parser.instantiate_classes(cfg)
         optimizer = init.optimizer([4.5, 6.7])
         assert isinstance(optimizer, Adam)
         assert [4.5, 6.7] == optimizer.params
         assert 0.01 == optimizer.lr
+        assert 0.0 == optimizer.momentum
         dump = parser.dump(cfg)
         assert yaml.safe_load(dump) == cfg.as_dict()
 
@@ -693,6 +696,44 @@ def test_callable_args_return_type_class(parser, subtests):
             assert f"{__name__}.{name}" in help_str
 
 
+def test_callable_multiple_args_return_type_class(parser, subtests):
+    parser.add_argument("--optimizer", type=Callable[[List[float], float], Optimizer], default=SGD)
+
+    with subtests.test("default"):
+        cfg = parser.get_defaults()
+        init = parser.instantiate_classes(cfg)
+        optimizer = init.optimizer([0.1, 2, 3], 1e-3)
+        assert isinstance(optimizer, SGD)
+        assert [0.1, 2, 3] == optimizer.params
+        assert 1e-3 == optimizer.lr
+        assert 0.0 == optimizer.momentum
+
+    with subtests.test("parse dict"):
+        value = {
+            "class_path": "Adam",
+            "init_args": {"momentum": 0.9},
+        }
+        cfg = parser.parse_args([f"--optimizer={value}"])
+        assert f"{__name__}.Adam" == cfg.optimizer.class_path
+        assert Namespace(momentum=0.9) == cfg.optimizer.init_args
+        init = parser.instantiate_classes(cfg)
+        optimizer = init.optimizer([4.5, 6.7], 0.01)
+        assert isinstance(optimizer, Adam)
+        assert [4.5, 6.7] == optimizer.params
+        assert 0.01 == optimizer.lr
+        assert 0.9 == optimizer.momentum
+        dump = parser.dump(cfg)
+        assert yaml.safe_load(dump) == cfg.as_dict()
+
+    with subtests.test("short notation"):
+        assert cfg == parser.parse_args(["--optimizer=Adam", "--optimizer.momentum=0.9"])
+
+    with subtests.test("help"):
+        help_str = get_parser_help(parser)
+        for name in ["Optimizer", "SGD", "Adam"]:
+            assert f"{__name__}.{name}" in help_str
+
+
 class StepLR:
     def __init__(self, optimizer: Optimizer, last_epoch: int = -1):
         self.optimizer = optimizer