Repository: facebookresearch/hydra

Issue:
[Bug] Not able to Access Parent Fields in instantiate Interpolation
# 🐛 Bug
## Description

Followup on #388 the parent fields seem to be getting lost again. If I have a field that is an interpolation of a field higher up in the hierarchy, I can print out the value fine but I can't use it within instiantiate.

## Checklist
- [x] I checked on the latest version of Hydra
- [x] I created a minimal repro

## To reproduce

** Minimal Code/Config snippet to reproduce **

**Minimal** code snippet which should print three times the same integer value. 

The first print is the parent field
The second print accesses the child field, which is an interpolation of the parent field
The third print uses instantiate to create an object which takes the child field as a parameter and prints from that object

Before the third print happens the exception is thrown

```
import time
import hydra
import submitit

class GetsTheInteger:
    def __init__(self, same_integer):
        self.intval = intval
        

@hydra.main(config_name="test2.yaml")
def main(cfg) -> None:
    print(cfg.data.integer)
    print(cfg.data.test.same_integer)

    g = hydra.utils.instantiate(cfg.data.test)
    print(g.intval)    


if __name__ == "__main__":
    main()
```

** Stack trace/error message **
```
Traceback (most recent call last):
  File "/private/home/mehrlich/.conda/envs/qgac/lib/python3.7/site-packages/hydra/_internal/utils.py", line 203, in run_and_report
    return func()
  File "/private/home/mehrlich/.conda/envs/qgac/lib/python3.7/site-packages/hydra/_internal/utils.py", line 355, in <lambda>
    overrides=args.overrides,
  File "/private/home/mehrlich/.conda/envs/qgac/lib/python3.7/site-packages/hydra/_internal/hydra.py", line 110, in run
    job_subdir_key=None,
  File "/private/home/mehrlich/.conda/envs/qgac/lib/python3.7/site-packages/hydra/core/utils.py", line 123, in run_job
    ret.return_value = task_function(task_cfg)
  File "test2.py", line 15, in main
    g = hydra.utils.instantiate(cfg.data.test)
  File "/private/home/mehrlich/.conda/envs/qgac/lib/python3.7/site-packages/hydra/utils.py", line 68, in call
    raise HydraException(f"Error calling '{cls}' : {e}") from e
hydra.errors.HydraException: Error calling 'test2.GetsTheInteger' : str interpolation key 'data.integer' not found
        full_key: same_integer
        reference_type=Any
        object_type=dict
```

## Expected Behavior

No crash, can instantiate objects whose parameters depend on interpolations of parent fields

## System information
- **Hydra Version** :  git master
- **Python version** : 3.7
- **Virtual environment type and version** : Conda
- **Operating system** :  Ubuntu 18.04 (fair cluster)


Discussion:
Your example is incomplete, where is the config file?
Oops completely forgot to add it

```
data:
  integer: 10
  test:
    _target_: test2.GetsTheInteger
    same_integer: ${data.integer}
```

Resolving patch:
diff --git a/hydra/utils.py b/hydra/utils.py
--- a/hydra/utils.py
+++ b/hydra/utils.py
@@ -1,5 +1,4 @@
 # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
-import copy
 import logging.config
 import os
 from pathlib import Path
@@ -39,20 +38,22 @@ def call(config: Any, *args: Any, **kwargs: Any) -> Any:
             f"\nA common problem is forgetting to annotate _target_ as a string : '_target_: str = ...'"
         )
 
-    if (
+    if not (
         isinstance(config, dict)
         or OmegaConf.is_config(config)
         or is_structured_config(config)
     ):
-        config = OmegaConf.structured(config)
-    else:
         raise HydraException(f"Unsupported config type : {type(config).__name__}")
 
+    # make a copy to ensure we do not change the provided object
+    config_copy = OmegaConf.structured(config)
+    if OmegaConf.is_config(config):
+        config_copy._set_parent(config._get_parent())
+    config = config_copy
+
     cls = "<unknown>"
     try:
         assert isinstance(config, DictConfig)
-        # make a copy to ensure we do not change the provided object
-        config = copy.deepcopy(config)
         OmegaConf.set_readonly(config, False)
         OmegaConf.set_struct(config, False)
         cls = _get_cls_name(config)


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -156,123 +156,42 @@ def test(conf: Any) -> None:
     "input_conf, passthrough, expected",
     [
         pytest.param(
-            {"_target_": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
-            {"a": 10},
-            AClass(10, 200, 200, "default_value"),
-            id="instantiate_respects_default_value",
-        )
+            {
+                "node": {
+                    "_target_": "tests.AClass",
+                    "a": "${value}",
+                    "b": 20,
+                    "c": 30,
+                    "d": 40,
+                },
+                "value": 99,
+            },
+            {},
+            AClass(99, 20, 30, 40),
+            id="interpolation_into_parent",
+        ),
     ],
 )
-def test_instantiate_respects_default_value_with_params(
-    input_conf: Any, passthrough: Dict[str, Any], expected: Any, recwarn: Any
+def test_interpolation_accessing_parent(
+    input_conf: Any, passthrough: Dict[str, Any], expected: Any
 ) -> Any:
-    conf = OmegaConf.create(input_conf)
-    assert isinstance(conf, DictConfig)
-    conf_copy = copy.deepcopy(conf)
-    obj = utils.instantiate(conf, **passthrough)
+    input_conf = OmegaConf.create(input_conf)
+    obj = utils.instantiate(input_conf.node, **passthrough)
     assert obj == expected
-    # make sure config is not modified by instantiate
-    assert conf == conf_copy
 
 
 @pytest.mark.parametrize(  # type: ignore
     "input_conf, passthrough, expected",
     [
         pytest.param(
-            {"target": "tests.AClass", "params": {"a": 10, "b": 20, "c": 30, "d": 40}},
-            {},
-            AClass(10, 20, 30, 40),
-            id="class",
-        ),
-        pytest.param(
-            {"target": "tests.AClass", "params": {"b": 20, "c": 30}},
-            {"a": 10, "d": 40},
-            AClass(10, 20, 30, 40),
-            id="class+override",
-        ),
-        pytest.param(
-            {"target": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
-            {"a": 10, "b": 99, "d": 40},
-            AClass(10, 99, 99, 40),
-            id="class+override+interpolation",
-        ),
-        # Check class and static methods
-        pytest.param(
-            {"target": "tests.ASubclass.class_method", "params": {"y": 10}},
-            {},
-            ASubclass(11),
-            id="class_method",
-        ),
-        pytest.param(
-            {"target": "tests.AClass.static_method", "params": {"z": 43}},
-            {},
-            43,
-            id="static_method",
-        ),
-        # Check nested types and static methods
-        pytest.param(
-            {"target": "tests.NestingClass", "params": {}},
-            {},
-            NestingClass(ASubclass(10)),
-            id="class_with_nested_class",
-        ),
-        pytest.param(
-            {"target": "tests.nesting.a.class_method", "params": {"y": 10}},
-            {},
-            ASubclass(11),
-            id="class_method_on_an_object_nested_in_a_global",
-        ),
-        pytest.param(
-            {"target": "tests.nesting.a.static_method", "params": {"z": 43}},
-            {},
-            43,
-            id="static_method_on_an_object_nested_in_a_global",
-        ),
-        # Check that default value is respected
-        pytest.param(
-            {"target": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
+            {"_target_": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
             {"a": 10},
             AClass(10, 200, 200, "default_value"),
             id="instantiate_respects_default_value",
-        ),
-        pytest.param(
-            {"target": "tests.AClass", "params": {}},
-            {"a": 10, "b": 20, "c": 30},
-            AClass(10, 20, 30, "default_value"),
-            id="instantiate_respects_default_value",
-        ),
-        # call a function from a module
-        pytest.param(
-            {"target": "tests.module_function", "params": {"x": 43}},
-            {},
-            43,
-            id="call_function_in_module",
-        ),
-        # Check builtins
-        pytest.param(
-            {"target": "builtins.str", "params": {"object": 43}},
-            {},
-            "43",
-            id="builtin_types",
-        ),
-        # Check that none is instantiated correctly
-        pytest.param(None, {}, None, id="instantiate_none",),
-        # passthrough
-        pytest.param(
-            {"target": "tests.AClass"},
-            {"a": 10, "b": 20, "c": 30},
-            AClass(a=10, b=20, c=30),
-            id="passthrough",
-        ),
-        pytest.param(
-            {"target": "tests.AClass"},
-            {"a": 10, "b": 20, "c": 30, "d": {"x": IllegalType()}},
-            AClass(a=10, b=20, c=30, d={"x": IllegalType()}),
-            id="passthrough",
-        ),
+        )
     ],
 )
-def test_class_instantiate_deprecated(
+def test_instantiate_respects_default_value_with_params(
     input_conf: Any, passthrough: Dict[str, Any], expected: Any, recwarn: Any
 ) -> Any:
     conf = OmegaConf.create(input_conf)
@@ -460,6 +379,120 @@ def test_pass_extra_variables_objectconf() -> None:
     assert recwarn[1].message.args[0] == target_field_deprecated.format(field="target")
 
 
+##################################
+# Testing deprecated logic below #
+##################################
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "input_conf, passthrough, expected",
+    [
+        pytest.param(
+            {"target": "tests.AClass", "params": {"a": 10, "b": 20, "c": 30, "d": 40}},
+            {},
+            AClass(10, 20, 30, 40),
+            id="class",
+        ),
+        pytest.param(
+            {"target": "tests.AClass", "params": {"b": 20, "c": 30}},
+            {"a": 10, "d": 40},
+            AClass(10, 20, 30, 40),
+            id="class+override",
+        ),
+        pytest.param(
+            {"target": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
+            {"a": 10, "b": 99, "d": 40},
+            AClass(10, 99, 99, 40),
+            id="class+override+interpolation",
+        ),
+        # Check class and static methods
+        pytest.param(
+            {"target": "tests.ASubclass.class_method", "params": {"y": 10}},
+            {},
+            ASubclass(11),
+            id="class_method",
+        ),
+        pytest.param(
+            {"target": "tests.AClass.static_method", "params": {"z": 43}},
+            {},
+            43,
+            id="static_method",
+        ),
+        # Check nested types and static methods
+        pytest.param(
+            {"target": "tests.NestingClass", "params": {}},
+            {},
+            NestingClass(ASubclass(10)),
+            id="class_with_nested_class",
+        ),
+        pytest.param(
+            {"target": "tests.nesting.a.class_method", "params": {"y": 10}},
+            {},
+            ASubclass(11),
+            id="class_method_on_an_object_nested_in_a_global",
+        ),
+        pytest.param(
+            {"target": "tests.nesting.a.static_method", "params": {"z": 43}},
+            {},
+            43,
+            id="static_method_on_an_object_nested_in_a_global",
+        ),
+        # Check that default value is respected
+        pytest.param(
+            {"target": "tests.AClass", "params": {"b": 200, "c": "${params.b}"}},
+            {"a": 10},
+            AClass(10, 200, 200, "default_value"),
+            id="instantiate_respects_default_value",
+        ),
+        pytest.param(
+            {"target": "tests.AClass", "params": {}},
+            {"a": 10, "b": 20, "c": 30},
+            AClass(10, 20, 30, "default_value"),
+            id="instantiate_respects_default_value",
+        ),
+        # call a function from a module
+        pytest.param(
+            {"target": "tests.module_function", "params": {"x": 43}},
+            {},
+            43,
+            id="call_function_in_module",
+        ),
+        # Check builtins
+        pytest.param(
+            {"target": "builtins.str", "params": {"object": 43}},
+            {},
+            "43",
+            id="builtin_types",
+        ),
+        # Check that none is instantiated correctly
+        pytest.param(None, {}, None, id="instantiate_none",),
+        # passthrough
+        pytest.param(
+            {"target": "tests.AClass"},
+            {"a": 10, "b": 20, "c": 30},
+            AClass(a=10, b=20, c=30),
+            id="passthrough",
+        ),
+        pytest.param(
+            {"target": "tests.AClass"},
+            {"a": 10, "b": 20, "c": 30, "d": {"x": IllegalType()}},
+            AClass(a=10, b=20, c=30, d={"x": IllegalType()}),
+            id="passthrough",
+        ),
+    ],
+)
+def test_class_instantiate_deprecated(
+    input_conf: Any, passthrough: Dict[str, Any], expected: Any, recwarn: Any
+) -> Any:
+    conf = OmegaConf.create(input_conf)
+    assert isinstance(conf, DictConfig)
+    conf_copy = copy.deepcopy(conf)
+    obj = utils.instantiate(conf, **passthrough)
+    assert obj == expected
+    # make sure config is not modified by instantiate
+    assert conf == conf_copy
+
+
 def test_object_conf_deprecated() -> None:
     with pytest.warns(UserWarning) as recwarn:
         cfg = ObjectConf(target="tests.AClass", params={"a": 10, "b": 20})
@@ -467,3 +500,28 @@ def test_object_conf_deprecated() -> None:
     assert ret == AClass(a=10, b=20, c=30)
     assert recwarn[0].message.args[0] == objectconf_depreacted
     assert recwarn[1].message.args[0] == target_field_deprecated.format(field="target")
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "input_conf, passthrough, expected",
+    [
+        pytest.param(
+            {
+                "node": {
+                    "cls": "tests.AClass",
+                    "params": {"a": "${value}", "b": 20, "c": 30, "d": 40},
+                },
+                "value": 99,
+            },
+            {},
+            AClass(99, 20, 30, 40),
+            id="deprecated:interpolation_into_parent",
+        ),
+    ],
+)
+def test_interpolation_accessing_parent_deprecated(
+    input_conf: Any, passthrough: Dict[str, Any], expected: Any, recwarn: Any
+) -> Any:
+    input_conf = OmegaConf.create(input_conf)
+    obj = utils.instantiate(input_conf.node, **passthrough)
+    assert obj == expected