Repository: mit-ll-responsible-ai/hydra-zen
Pull request #261 (2022-04-15 02:54:01)

Enable nested container types for omegaconf 2.2.0
omegaconf 2.2.0 adds supports for nested container types: https://github.com/omry/omegaconf/pull/890

Previously, hydra-zen applied automatic type-widening for nested container types:

```python
import inspect

def f(x: List[Dict[int, List[str]]]): pass

Conf = builds(f, populate_full_signature=True)

inspect.signature(Conf)  # <Signature (x: List[Any]) -> None>
```

Now, if `omegaconf >= 2.2.0` is installed, `builds` will permit nested container types.

```python
Conf = builds(f, populate_full_signature=True)

inspect.signature(Conf)  # <Signature (x: List[Dict[int, List[str]]]) -> None>
```

Type-widening still applies within nested containers:

```python
def f(x: List[List[Type[int]]): pass  # Type[int] is not supported

Conf = builds(f, populate_full_signature=True)

inspect.signature(Conf)  # <Signature (x: List[List[Any]]) -> None>
```



Patch:
diff --git a/src/hydra_zen/_compatibility.py b/src/hydra_zen/_compatibility.py
index a9353502..d5b3f21d 100644
--- a/src/hydra_zen/_compatibility.py
+++ b/src/hydra_zen/_compatibility.py
@@ -4,7 +4,7 @@ from collections import Counter, deque
 from enum import Enum
 from functools import partial
 from pathlib import Path, PosixPath, WindowsPath
-from typing import NamedTuple, Optional, Set
+from typing import NamedTuple, Set
 
 import hydra
 import omegaconf
@@ -49,11 +49,10 @@ PATCH_OMEGACONF_830: Final = True  # OMEGACONF_VERSION < Version(2, 1, 1)
 # Hydra's instantiate API now supports partial-instantiation, indicated
 # by a `_partial_ = True` attribute.
 # https://github.com/facebookresearch/hydra/pull/1905
-#
-# Uncomment dynamice setting of `HYDRA_SUPPORTS_PARTIAL` once we can
-# begin testing against nightly builds of Hydra
 HYDRA_SUPPORTS_PARTIAL: Final = Version(1, 1, 1) < HYDRA_VERSION
 
+HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES: Final = OMEGACONF_VERSION >= Version(2, 2, 0)
+
 # Indicates primitive types permitted in type-hints of structured configs
 HYDRA_SUPPORTED_PRIMITIVE_TYPES: Final = {int, float, bool, str, Enum}
 # Indicates types of primitive values permitted in configs
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index 90f4cf93..01837288 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -28,6 +28,7 @@ from typing_extensions import Final, TypeGuard
 
 from hydra_zen._compatibility import (
     HYDRA_SUPPORTED_PRIMITIVE_TYPES,
+    HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES,
     PATCH_OMEGACONF_830,
 )
 from hydra_zen.typing._implementations import DataClass_, Field, InterpStr
@@ -294,6 +295,7 @@ def sanitized_type(
     # Warning: mutating `type_` will mutate the signature being inspected
     # Even calling deepcopy(`type_`) silently fails to prevent this.
     origin = get_origin(type_)
+    no_nested_container = not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
 
     if origin is not None:
         if primitive_only:
@@ -319,13 +321,13 @@ def sanitized_type(
             return Union[optional_type, NoneType]  # type: ignore
 
         if origin is list or origin is List:
-            return List[sanitized_type(args[0], primitive_only=True)] if args else type_  # type: ignore
+            return List[sanitized_type(args[0], primitive_only=no_nested_container)] if args else type_  # type: ignore
 
         if origin is dict or origin is Dict:
             return (
                 Dict[
                     sanitized_type(args[0], primitive_only=True),  # type: ignore
-                    sanitized_type(args[1], primitive_only=True),  # type: ignore
+                    sanitized_type(args[1], primitive_only=no_nested_container),  # type: ignore
                 ]
                 if args
                 else type_
@@ -345,7 +347,7 @@ def sanitized_type(
             has_ellipses = Ellipsis in unique_args
 
             _unique_type = (
-                sanitized_type(args[0], primitive_only=True)
+                sanitized_type(args[0], primitive_only=no_nested_container)
                 if len(unique_args) == 1 or (len(unique_args) == 2 and has_ellipses)
                 else Any
             )


Test patch:
diff --git a/tests/test_third_party/test_type_validators.py b/tests/test_third_party/test_type_validators.py
index f3efa8d2..51528b92 100644
--- a/tests/test_third_party/test_type_validators.py
+++ b/tests/test_third_party/test_type_validators.py
@@ -18,7 +18,7 @@ from typing import (
 import hypothesis.strategies as st
 import pytest
 from hypothesis import assume, given, settings
-from omegaconf import OmegaConf
+from omegaconf import OmegaConf, ValidationError
 from typing_extensions import Annotated, Final, Literal, TypedDict
 
 from hydra_zen import builds, instantiate, to_yaml
@@ -207,7 +207,6 @@ class UserIdentity(TypedDict):
     "annotation, fools_hydra",
     [
         (Tuple[str, int, bool], (True, "hi", 2)),
-        (xf.add(Dict[str, int], BEAR), dict(a="hi")),
         (Literal["a", "b"], "c"),
         (Union[List[str], str], dict(a=1)),
         (MyNamedTuple, (1.0, 2.0)),
@@ -325,7 +324,13 @@ def test_signature_parsing(args, kwargs, should_pass, validator, target, as_yaml
             hydra_convert="all",
         )
         if as_yaml:
-            conf_with_val = OmegaConf.create(to_yaml(conf_with_val))
+
+            try:
+                # omegaconf >= 2.2.0 no longer casts lists to strings
+                conf_with_val = OmegaConf.create(to_yaml(conf_with_val))
+            except ValidationError:
+                assume(False)
+
         if not should_pass:
             with pytest.raises(Exception):
                 instantiate(conf_with_val)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 6a037582..48e66f72 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -21,7 +21,11 @@ from omegaconf.errors import (
 from typing_extensions import Final, Literal
 
 from hydra_zen import builds, instantiate, mutable_value
-from hydra_zen._compatibility import Version, _get_version
+from hydra_zen._compatibility import (
+    HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES,
+    Version,
+    _get_version,
+)
 from hydra_zen.structured_configs._utils import (
     field,
     is_interpolated_string,
@@ -139,22 +143,52 @@ NoneType = type(None)
         (Union[NoneType, int], Optional[int]),  # supported Optional
         (Optional[Color], Optional[Color]),
         (Optional[List[Color]], Optional[List[Color]]),
-        (Optional[List[List[int]]], Optional[List[Any]]),
+        (
+            Optional[List[List[int]]],
+            Optional[List[Any]]
+            if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
+            else Optional[List[List[int]]],
+        ),
         (List[int], List[int]),  # supported containers
         (List[frozenset], List[Any]),
-        (List[List[int]], List[Any]),
+        (
+            List[List[int]],
+            List[Any] if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES else List[List[int]],
+        ),
         (List[T], List[Any]),
         (Dict[str, float], Dict[str, float]),
         (Dict[C, int], Dict[Any, int]),
         (Dict[str, C], Dict[str, Any]),
         (Dict[C, C], Dict[Any, Any]),
-        (Dict[str, List[int]], Dict[str, Any]),
+        (
+            Dict[str, List[int]],
+            Dict[str, Any]
+            if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
+            else Dict[str, List[int]],
+        ),
         (Tuple[str], Tuple[str]),
         (Tuple[str, ...], Tuple[str, ...]),
         (Tuple[str, str, str], Tuple[str, str, str]),
-        (Tuple[List[int]], Tuple[Any]),
+        (
+            Tuple[List[int]],
+            (
+                Tuple[Any]
+                if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
+                else Tuple[List[int]]
+            ),
+        ),
         (Union[NoneType, Tuple[int, int]], Optional[Tuple[int, int]]),
         (Union[Tuple[int, int], NoneType], Optional[Tuple[int, int]]),
+        (
+            List[Dict[str, List[int]]],
+            List[Any]
+            if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
+            else List[Dict[str, List[int]]],
+        ),
+        (
+            List[List[Type[int]]],
+            List[Any] if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES else List[List[Any]],
+        ),
     ],
 )
 def test_sanitized_type_expected_behavior(in_type, expected_type):
@@ -180,7 +214,8 @@ def test_sanitized_type_expected_behavior(in_type, expected_type):
                 ConfigValueError,
             )
         ):
-            OmegaConf.create(Bad)
+            Conf = OmegaConf.create(Bad)
+            Conf.x = [[int]]  # special case: validate
 
     @dataclass
     class Tmp: