Repository: facebookresearch/hydra

Issue:
[Bug] issues with named arguments in hydra.utils.instantiate
`hydra.utils.instantiate` converts to OmegaConf named arguments, making it impossible to use the named arguments syntax for arguments which cannot be represented in OmegaConf.

## To reproduce:

`conf.yaml`:

```yaml
class: torch.optim.Adam
params:
  lr: 1E-4
```

`test.py`:

```python
import hydra
import torch
import torchvision

@hydra.main(config_path="conf.yaml")
def my_app(cfg):
    print(cfg.pretty())
    model = torchvision.models.alexnet()
    hydra.utils.instantiate(cfg, model.parameters())  # OK
    hydra.utils.instantiate(cfg, params=model.parameters())  # FAILS

if __name__ == "__main__":
    my_app()
```

** Stack trace/error message **
```
ValueError: key params: generator is not a primitive type
```

As a side note, the fact that `hydra.utils.instantiate` takes as input an argument named `config` makes it impossible to instantiate an object which expects the same argument using named arguments, which seems an unnecessary restriction.

## The following solves both issues:

```python
def instantiate_f(config):
    assert config is not None, "Input config is None"
    class_name = config["class"]
    clazz = get_class(class_name)
    params = config.params if "params" in config else OmegaConf.create()
    assert isinstance(
        params, DictConfig
    ), "Input config params are expected to be a mapping, found {}".format(
        type(config.params)
    )
    params = OmegaConf.to_container(params, resolve=True)

    def f(*args, **kwargs):
        try:
            kwargs = {**params, **kwargs}
            return clazz(*args, **kwargs)
        except Exception as e:
            log.error("Error instantiating {} : {}".format(class_name, e))
            raise e

    return f

@hydra.main(config_path="conf.yaml")
def my_app(cfg):
    print(cfg.pretty())
    model = torchvision.models.alexnet()
    instantiate_f(cfg)(params=model.parameters())  # OK
    instantiate_f(cfg)(model.parameters())  # OK

if __name__ == "__main__":
    my_app()
```

A note in the documentation should be added explaining the behavior wrt `config`, as it gets resolved immediately.


Discussion:
Thanks for the issue report. will look into addressing it in a bit cleaner when when I have a chance.
support for passthrough was first added exactly for this use case (optimizer taking parameters()). named parameter support was  added later which explains why this issue went unnoticed by me.

Resolving patch:
diff --git a/hydra/conf/__init__.py b/hydra/conf/__init__.py
--- a/hydra/conf/__init__.py
+++ b/hydra/conf/__init__.py
@@ -25,6 +25,8 @@
 
 
 @dataclass
+# This extends Dict[str, Any] to allow for the deprecated "class" field.
+# Once support for class field removed this can stop extending Dict.
 class PluginConf(Dict[str, Any]):
     # class name for plugin
     cls: str = MISSING
diff --git a/hydra/utils.py b/hydra/utils.py
--- a/hydra/utils.py
+++ b/hydra/utils.py
@@ -4,7 +4,7 @@
 from pathlib import Path
 from typing import Any
 
-from omegaconf import DictConfig, OmegaConf
+from omegaconf import DictConfig, OmegaConf, _utils
 
 from hydra.conf import PluginConf
 from hydra.core.hydra_config import HydraConfig
@@ -67,9 +67,22 @@ def instantiate(config: PluginConf, *args: Any, **kwargs: Any) -> Any:
         ), "Input config params are expected to be a mapping, found {}".format(
             type(config.params)
         )
-        params.merge_with(OmegaConf.create(kwargs))
-
-        return clazz(*args, **params)
+        primitives = {}
+        rest = {}
+        for k, v in kwargs.items():
+            if _utils._is_primitive_type(v) or isinstance(v, (dict, list)):
+                primitives[k] = v
+            else:
+                rest[k] = v
+        final_kwargs = {}
+        params.merge_with(OmegaConf.create(primitives))
+        for k, v in params.items():
+            final_kwargs[k] = v
+
+        for k, v in rest.items():
+            final_kwargs[k] = v
+
+        return clazz(*args, **final_kwargs)
     except Exception as e:
         log.error(f"Error instantiating '{classname}' : {e}")
         raise e


Test patch:
diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py
--- a/tests/test_tutorials.py
+++ b/tests/test_tutorials.py
@@ -205,7 +205,7 @@ def test_tutorial_defaults(tmpdir: Path, args: List[str], expected: DictConfig)
             OmegaConf.create(
                 {
                     "db": {
-                        "class": "tutorial.objects_example.my_app.MySQLConnection",
+                        "cls": "tutorial.objects_example.my_app.MySQLConnection",
                         "params": {
                             "host": "localhost",
                             "user": "root",
@@ -220,7 +220,7 @@ def test_tutorial_defaults(tmpdir: Path, args: List[str], expected: DictConfig)
             OmegaConf.create(
                 {
                     "db": {
-                        "class": "tutorial.objects_example.my_app.MySQLConnection",
+                        "cls": "tutorial.objects_example.my_app.MySQLConnection",
                         "params": {
                             "host": "localhost",
                             "user": "root",
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,6 +7,7 @@
 
 import hydra._internal.utils as internal_utils
 from hydra import utils
+from hydra.conf import PluginConf
 from hydra.core.hydra_config import HydraConfig
 
 
@@ -48,6 +49,16 @@ def __ne__(self, other: Any) -> Any:
         return NotImplemented
 
 
+class Foo:
+    def __init__(self, x: int) -> None:
+        self.x = x
+
+    def __eq__(self, other: Any) -> Any:
+        if isinstance(other, Foo):
+            return self.x == other.x
+        return False
+
+
 @pytest.mark.parametrize("path,expected_type", [("tests.test_utils.Bar", Bar)])  # type: ignore
 def test_get_class(path: str, expected_type: type) -> None:
     assert utils.get_class(path) == expected_type
@@ -103,6 +114,12 @@ def test_get_static_method(path: str, return_value: Any) -> None:
             {"a": 10, "d": 40},
             Bar(10, 200, 200, 40),
         ),
+        (
+            {"cls": "tests.test_utils.Bar", "params": {"b": 200, "c": "${params.b}"}},
+            None,
+            {"a": 10, "d": Foo(99)},
+            Bar(10, 200, 200, Foo(99)),
+        ),
     ],
 )
 def test_class_instantiate(
@@ -121,6 +138,18 @@ def test_class_instantiate(
     assert obj == expected
 
 
+def test_class_instantiate_pass_omegaconf_node() -> Any:
+    pc = PluginConf()
+    # This is a bit clunky because it exposes a problem with the backport of dataclass on Python 3.6
+    # see: https://github.com/ericvsmith/dataclasses/issues/155
+    pc.cls = "tests.test_utils.Bar"
+    pc.params = {"b": 200, "c": {"x": 10, "y": "${params.b}"}}
+    conf = OmegaConf.structured(pc)
+    obj = utils.instantiate(conf, **{"a": 10, "d": Foo(99)})
+    assert obj == Bar(10, 200, {"x": 10, "y": 200}, Foo(99))
+    assert OmegaConf.is_config(obj.c)
+
+
 def test_class_warning() -> None:
     expected = Bar(10, 20, 30, 40)
     with pytest.warns(UserWarning):