Repository: mit-ll-responsible-ai/hydra-zen

Issue:
Improving our internal code organization for handling cross-version compatibility
Issues #172 , #181, and #182 all pertain to fixes/capabilities being introduced to omegaconf/Hydra that we already support. Thus we need to adaptively defer to upstream support for these capabilities when users have sufficiently-new version of omageconf/Hydra installed. 

Currently, we don't really have any tooling for making this process particularly centralized or simple. It would be good to have any fields, functions, and other objects that need be set dynamically based on omegaconf/Hydra versions all live in a common place.

Additionally, we should improve our CI to test against master branches of Hydra and omegaconf, since those projects don't seem to be pushing dev versions to pypi lately.

  

Discussion:

Patch:
diff --git a/src/hydra_zen/_compatibility.py b/src/hydra_zen/_compatibility.py
new file mode 100644
index 00000000..921a7e10
--- /dev/null
+++ b/src/hydra_zen/_compatibility.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2021 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+from collections import Counter, deque
+from enum import Enum
+from pathlib import Path, PosixPath, WindowsPath
+from typing import NamedTuple, Optional, Set
+
+import hydra
+import omegaconf
+from typing_extensions import Final
+
+NoneType = type(None)
+
+
+class Version(NamedTuple):
+    major: int
+    minor: int
+    patch: Optional[int] = None
+
+
+def _get_version(ver_str: str) -> Version:
+    # Not for general use. Tested only for Hydra and OmegaConf
+    # version string styles
+    major, minor, *_ = (int(v) for v in ver_str.split("."))
+    return Version(major=major, minor=minor)
+
+
+OMEGACONF_VERSION: Final = _get_version(omegaconf.__version__)
+HYDRA_VERSION: Final = _get_version(hydra.__version__)
+
+
+# OmegaConf issue 830 describes a bug associated with structured configs
+# composed via inheritance, where the child's attribute is a default-factory
+# and the parent's corresponding attribute is not.
+# We provide downstream workarounds until an upstream fix is released.
+#
+# Uncomment dynamic setting once OmegaConf merges fix:
+# https://github.com/omry/omegaconf/pull/832
+PATCH_OMEGACONF_830: Final = True  # OMEGACONF_VERSION <= Version(2, 1)
+
+# Hydra's instantiate API now supports partial-instantiation, indicated
+# by a `_partial_ = True` attribute.
+# https://github.com/facebookresearch/hydra/pull/1905
+HYDRA_SUPPORTS_PARTIAL: Final = Version(1, 1) < HYDRA_VERSION
+
+# 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
+HYDRA_SUPPORTED_PRIMITIVES = {int, float, bool, str, list, tuple, dict, NoneType}
+ZEN_SUPPORTED_PRIMITIVES: Set[type] = {
+    set,
+    frozenset,
+    complex,
+    Path,
+    PosixPath,
+    WindowsPath,
+    bytes,
+    bytearray,
+    deque,
+    Counter,
+    range,
+}
diff --git a/src/hydra_zen/_hydra_overloads.py b/src/hydra_zen/_hydra_overloads.py
index 1a911604..2573a319 100644
--- a/src/hydra_zen/_hydra_overloads.py
+++ b/src/hydra_zen/_hydra_overloads.py
@@ -26,7 +26,7 @@ from hydra.utils import instantiate as hydra_instantiate
 from omegaconf import MISSING, DictConfig, ListConfig, OmegaConf
 
 from .typing import Builds, Just, Partial, PartialBuilds
-from .typing._implementations import _DataClass
+from .typing._implementations import HydraPartialBuilds, _DataClass
 
 __all__ = ["instantiate", "to_yaml", "save_as_yaml", "load_from_yaml", "MISSING"]
 
@@ -41,6 +41,26 @@ def instantiate(
     ...
 
 
+@overload
+def instantiate(
+    config: Union[
+        HydraPartialBuilds[Callable[..., T]], Type[HydraPartialBuilds[Callable[..., T]]]
+    ],
+    *args: Any,
+    **kwargs: Any
+) -> Partial[T]:  # pragma: no cover
+    ...
+
+
+@overload
+def instantiate(
+    config: Union[HydraPartialBuilds[Type[T]], Type[HydraPartialBuilds[Type[T]]]],
+    *args: Any,
+    **kwargs: Any
+) -> Partial[T]:  # pragma: no cover
+    ...
+
+
 @overload
 def instantiate(
     config: Union[
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index bb6d0aa4..82e51f43 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -40,6 +40,12 @@ from typing import (
 from omegaconf import DictConfig, ListConfig
 from typing_extensions import Final, Literal, TypeGuard
 
+from hydra_zen._compatibility import (
+    HYDRA_SUPPORTED_PRIMITIVES,
+    HYDRA_SUPPORTS_PARTIAL,
+    PATCH_OMEGACONF_830,
+    ZEN_SUPPORTED_PRIMITIVES,
+)
 from hydra_zen.errors import (
     HydraZenDeprecationWarning,
     HydraZenUnsupportedPrimitiveError,
@@ -55,7 +61,7 @@ from hydra_zen.typing._implementations import (
     _DataClass,
 )
 
-from ._value_conversion import ZEN_SUPPORTED_PRIMITIVES, ZEN_VALUE_CONVERSION
+from ._value_conversion import ZEN_VALUE_CONVERSION
 
 _T = TypeVar("_T")
 _T2 = TypeVar("_T2", bound=Callable)
@@ -77,6 +83,7 @@ else:
 
 # Hydra-specific fields
 _TARGET_FIELD_NAME: Final[str] = "_target_"
+_PARTIAL_FIELD_NAME: Final[str] = "_partial_"
 _RECURSIVE_FIELD_NAME: Final[str] = "_recursive_"
 _CONVERT_FIELD_NAME: Final[str] = "_convert_"
 _POS_ARG_FIELD_NAME: Final[str] = "_args_"
@@ -93,7 +100,7 @@ _HYDRA_FIELD_NAMES: FrozenSet[str] = frozenset(
 # hydra-zen-specific fields
 _ZEN_PROCESSING_LOCATION: Final[str] = _utils.get_obj_path(zen_processing)
 _ZEN_TARGET_FIELD_NAME: Final[str] = "_zen_target"
-_PARTIAL_TARGET_FIELD_NAME: Final[str] = "_zen_partial"
+_ZEN_PARTIAL_TARGET_FIELD_NAME: Final[str] = "_zen_partial"
 _META_FIELD_NAME: Final[str] = "_zen_exclude"
 _ZEN_WRAPPERS_FIELD_NAME: Final[str] = "_zen_wrappers"
 _JUST_FIELD_NAME: Final[str] = "path"
@@ -106,9 +113,6 @@ _VAR_POSITIONAL: Final = inspect.Parameter.VAR_POSITIONAL
 _KEYWORD_ONLY: Final = inspect.Parameter.KEYWORD_ONLY
 _VAR_KEYWORD: Final = inspect.Parameter.VAR_KEYWORD
 
-NoneType = type(None)
-HYDRA_SUPPORTED_PRIMITIVES = {int, float, bool, str, list, tuple, dict, NoneType}
-
 _builtin_function_or_method_type = type(len)
 
 
@@ -409,7 +413,7 @@ def hydrated_dataclass(
         decorated_obj = cast(Any, decorated_obj)
         decorated_obj = dataclass(frozen=frozen)(decorated_obj)
 
-        if _utils.PATCH_OMEGACONF_830 and 2 < len(decorated_obj.__mro__):
+        if PATCH_OMEGACONF_830 and 2 < len(decorated_obj.__mro__):
             parents = decorated_obj.__mro__[1:-1]
             # this class inherits from a parent
             for field_ in fields(decorated_obj):
@@ -1200,7 +1204,28 @@ def builds(
 
     target_field: List[Union[Tuple[str, Type[Any]], Tuple[str, Type[Any], Any]]]
 
-    if zen_partial or zen_meta or validated_wrappers:
+    if (
+        HYDRA_SUPPORTS_PARTIAL
+        and zen_partial
+        # check that no other zen-processing is needed
+        and not zen_meta
+        and not validated_wrappers
+    ):  # pragma: no cover
+        # TODO: require test-coverage once Hydra publishes nightly builds
+        target_field = [
+            (
+                _TARGET_FIELD_NAME,
+                str,
+                _utils.field(default=_utils.get_obj_path(target), init=False),
+            ),
+            (
+                _PARTIAL_FIELD_NAME,
+                str,
+                _utils.field(default=zen_partial, init=False),
+            ),
+        ]
+    elif zen_partial or zen_meta or validated_wrappers:
+        # target is `hydra_zen.funcs.zen_processing`
         target_field = [
             (
                 _TARGET_FIELD_NAME,
@@ -1217,7 +1242,7 @@ def builds(
         if zen_partial:
             target_field.append(
                 (
-                    _PARTIAL_TARGET_FIELD_NAME,
+                    _ZEN_PARTIAL_TARGET_FIELD_NAME,
                     bool,
                     _utils.field(default=True, init=False),
                 ),
@@ -1587,7 +1612,7 @@ def builds(
                     ),
                 )
             elif (
-                _utils.PATCH_OMEGACONF_830
+                PATCH_OMEGACONF_830
                 and builds_bases
                 and value.default_factory is not MISSING
             ):
@@ -1635,7 +1660,7 @@ def builds(
         dataclass_name, fields=sanitized_base_fields, bases=builds_bases, frozen=frozen
     )
 
-    if zen_partial is False and hasattr(out, _PARTIAL_TARGET_FIELD_NAME):
+    if zen_partial is False and hasattr(out, _ZEN_PARTIAL_TARGET_FIELD_NAME):
         # `out._partial_target_` has been inherited; this will lead to an error when
         # hydra-instantiation occurs, since it will be passed to target.
         # There is not an easy way to delete this, since it comes from a parent class
@@ -1710,7 +1735,13 @@ def uses_zen_processing(x: Any) -> TypeGuard[Builds]:
 
 def is_partial_builds(x: Any) -> TypeGuard[PartialBuilds]:
     return (
-        uses_zen_processing(x) and getattr(x, _PARTIAL_TARGET_FIELD_NAME, False) is True
+        # check if partial'd config via Hydra
+        HYDRA_SUPPORTS_PARTIAL
+        and getattr(x, _PARTIAL_FIELD_NAME, False) is True
+    ) or (
+        # check if partial'd config via `zen_processing`
+        uses_zen_processing(x)
+        and (getattr(x, _ZEN_PARTIAL_TARGET_FIELD_NAME, False) is True)
     )
 
 
@@ -1806,7 +1837,7 @@ def get_target(obj: Union[HasTarget, HasPartialTarget]) -> Any:
     else:
         raise TypeError(
             f"`obj` must specify a target; i.e. it must have an attribute named"
-            f" {_TARGET_FIELD_NAME} or named {_PARTIAL_TARGET_FIELD_NAME} that"
+            f" {_TARGET_FIELD_NAME} or named {_ZEN_PARTIAL_TARGET_FIELD_NAME} that"
             f" points to a target-object or target-string"
         )
     target = getattr(obj, field_name)
@@ -2294,7 +2325,7 @@ def make_config(
         if not isinstance(value, ZenField):
             default_factory_permitted = (
                 not bases or _utils.mutable_default_permitted(bases, field_name=name)
-                if _utils.PATCH_OMEGACONF_830
+                if PATCH_OMEGACONF_830
                 else True
             )
             normalized_fields[name] = ZenField(
@@ -2356,7 +2387,7 @@ def _repack_zenfield(value: ZenField, name: str, bases: Tuple[_DataClass, ...]):
     default = value.default
 
     if (
-        _utils.PATCH_OMEGACONF_830
+        PATCH_OMEGACONF_830
         and bases
         and not _utils.mutable_default_permitted(bases, field_name=name)
         and isinstance(default, Field)
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index 2f446ca4..6ce1ac20 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -24,10 +24,12 @@ from typing import (
 from omegaconf import II
 from typing_extensions import Final, TypeGuard
 
+from hydra_zen._compatibility import (
+    HYDRA_SUPPORTED_PRIMITIVE_TYPES,
+    PATCH_OMEGACONF_830,
+)
 from hydra_zen.typing._implementations import InterpStr, _DataClass
 
-PATCH_OMEGACONF_830: Final[bool] = True
-
 try:
     from typing import get_args, get_origin
 except ImportError:  # pragma: no cover
@@ -98,8 +100,6 @@ COMMON_MODULES_WITH_OBFUSCATED_IMPORTS: Tuple[str, ...] = (
     "torch",
 )
 UNKNOWN_NAME: Final[str] = "<unknown>"
-HYDRA_SUPPORTED_PRIMITIVE_TYPES: Final = {int, float, bool, str, Enum}
-
 KNOWN_MUTABLE_TYPES = {list, dict, set}
 
 T = TypeVar("T")
diff --git a/src/hydra_zen/structured_configs/_value_conversion.py b/src/hydra_zen/structured_configs/_value_conversion.py
index 6f18d8f7..aef6fdff 100644
--- a/src/hydra_zen/structured_configs/_value_conversion.py
+++ b/src/hydra_zen/structured_configs/_value_conversion.py
@@ -1,27 +1,15 @@
-from collections import Counter, deque
+# Copyright (c) 2021 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+
 from dataclasses import dataclass
 from pathlib import Path, PosixPath, WindowsPath
-from typing import Any, Callable, Dict, Set, Type, cast
+from typing import Any, Callable, Dict, Type, cast
 
 from hydra_zen.typing import Builds
 
 from ._utils import get_obj_path
 
 # Some primitive support implemented in _implementations.py
-ZEN_SUPPORTED_PRIMITIVES: Set[type] = {
-    set,
-    frozenset,
-    complex,
-    Path,
-    PosixPath,
-    WindowsPath,
-    bytes,
-    bytearray,
-    deque,
-    Counter,
-    range,
-}
-
 ZEN_VALUE_CONVERSION: Dict[type, Callable[[Any], Any]] = {}
 
 
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 2494de48..7159cfcf 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -66,16 +66,15 @@ class DataClass(_DataClass, Protocol):  # pragma: no cover
     def __init__(self, *args, **kwargs) -> None:
         ...
 
-    def __getattribute__(self, name: str) -> Any:
+    def __getattribute__(self, __name: str) -> Any:
         ...
 
-    def __setattr__(self, name: str, value: Any) -> None:
+    def __setattr__(self, __name: str, __value: Any) -> None:
         ...
 
 
 @runtime_checkable
 class Builds(DataClass, Protocol[_T]):  # pragma: no cover
-
     _target_: str
 
 
@@ -92,6 +91,11 @@ class PartialBuilds(Builds, Protocol[_T]):  # pragma: no cover
     _zen_partial: bool = True
 
 
+@runtime_checkable
+class HydraPartialBuilds(Builds, Protocol[_T]):  # pragma: no cover
+    _partial_: bool = True
+
+
 @runtime_checkable
 class HasTarget(Protocol):  # pragma: no cover
     _target_: str


Test patch:
diff --git a/tests/annotations/declarations.py b/tests/annotations/declarations.py
index b0227fef..8adc74ab 100644
--- a/tests/annotations/declarations.py
+++ b/tests/annotations/declarations.py
@@ -5,7 +5,7 @@ from collections import Counter, deque
 from dataclasses import dataclass
 from enum import Enum
 from pathlib import Path
-from typing import Callable, List, Literal, Tuple, Type
+from typing import Callable, List, Literal, Tuple, Type, TypeVar
 
 from omegaconf import MISSING, DictConfig, ListConfig
 
@@ -20,6 +20,9 @@ from hydra_zen import (
     mutable_value,
 )
 from hydra_zen.typing import Builds
+from hydra_zen.typing._implementations import HydraPartialBuilds
+
+T = TypeVar("T")
 
 
 class A:
@@ -338,3 +341,12 @@ def check_inheritance():
     # make_config(x=1, bases=(lambda x: x,))
     # make_config(x=1, bases=(None,))
     # make_config(x=1, bases=(A,))
+
+
+def make_hydra_partial(x: T) -> HydraPartialBuilds[Type[T]]:
+    ...
+
+
+def check_HydraPartialBuilds():
+    cfg = make_hydra_partial(int)
+    a: Literal["Partial[int]"] = reveal_type(instantiate(cfg))
diff --git a/tests/test_compatibility/test_hydra_supports_partial.py b/tests/test_compatibility/test_hydra_supports_partial.py
new file mode 100644
index 00000000..114b5295
--- /dev/null
+++ b/tests/test_compatibility/test_hydra_supports_partial.py
@@ -0,0 +1,95 @@
+from dataclasses import dataclass
+from functools import partial
+
+import pytest
+
+from hydra_zen import builds, get_target, instantiate, make_custom_builds_fn
+from hydra_zen._compatibility import HYDRA_SUPPORTS_PARTIAL
+from hydra_zen.structured_configs._implementations import (
+    is_partial_builds,
+    uses_zen_processing,
+)
+from hydra_zen.structured_configs._utils import get_obj_path
+from hydra_zen.typing import PartialBuilds
+from hydra_zen.typing._implementations import HydraPartialBuilds
+
+
+@dataclass
+class HydraPartialConf:
+    _target_: str = get_obj_path(dict)
+    _partial_: bool = True
+    x: int = 1
+
+
+def test_HydraPartialBuilds_protocol():
+    assert isinstance(HydraPartialConf(), HydraPartialBuilds)
+    assert not isinstance(HydraPartialConf(), PartialBuilds)
+
+
+def test_HYDRA_SUPPORTS_PARTIAL_is_set_properly():
+
+    obj = instantiate(HydraPartialConf)
+    if HYDRA_SUPPORTS_PARTIAL:
+        # instantiation should produce `functools.partial(dict, x=1)`
+        assert callable(obj) and obj() == {"x": 1}
+    else:
+        # instantiation should product `dict(x=1, _partial_=True)`
+        assert obj == {"x": 1, "_partial_": True}
+
+
+def test_is_partial_builds_on_hydra_partial_config():
+    assert is_partial_builds(HydraPartialConf) is HYDRA_SUPPORTS_PARTIAL
+
+
+def test_get_target_on_hydra_partial_config():
+    assert get_target(HydraPartialConf) is dict
+
+
+@pytest.mark.parametrize("via_custom_builds", [False, True])
+def test_builds_leverages_hydra_support_for_partial_when_no_other_zen_processing_used(
+    via_custom_builds,
+):
+    builder = (
+        partial(builds, zen_partial=True)
+        if not via_custom_builds
+        else make_custom_builds_fn(zen_partial=True)
+    )
+    Conf = builder(dict, x=1)
+    assert uses_zen_processing(Conf) is not HYDRA_SUPPORTS_PARTIAL
+    assert hasattr(Conf, "_partial_") is HYDRA_SUPPORTS_PARTIAL
+    instance = instantiate(Conf)()
+    assert instance == {"x": 1}
+
+
+def f(*args, **kwargs):
+    return args, kwargs
+
+
+def identity_wrapper(f):
+    return f
+
+
+@pytest.mark.parametrize("zen_meta", [None, {"_y": 1}])
+@pytest.mark.parametrize("zen_wrapper", [(), identity_wrapper])
+@pytest.mark.parametrize("pop_sig", [False, True])
+def test_partial_func_attr_is_always_target(zen_meta, zen_wrapper, pop_sig):
+    # Regardless of whether Hydra performs partial instantiation or if zen_processing
+    # is responsible, the result of the instantiation should be a partial object
+    # whose `.func` attr is the actual target-object.
+    #
+    # In particular, we want to ensure that `.func` never points to `zen_processing`,
+    # which could still produce the same end behavior for resolving the partial, but
+    # would nonetheless have unexpected behaviors to those users who query `.func`.
+    Conf = builds(
+        f,
+        "hi",
+        x=1,
+        zen_meta=zen_meta,
+        zen_wrappers=zen_wrapper,
+        populate_full_signature=pop_sig,
+        zen_partial=True,
+    )
+
+    partial_out = instantiate(Conf)
+    assert hasattr(partial_out, "func") and partial_out.func is f  # type: ignore
+    assert partial_out() == (("hi",), {"x": 1})
diff --git a/tests/test_compatibility/test_omegaconf_830.py b/tests/test_compatibility/test_omegaconf_830.py
new file mode 100644
index 00000000..2c178b1e
--- /dev/null
+++ b/tests/test_compatibility/test_omegaconf_830.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2021 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+
+from dataclasses import dataclass, field, is_dataclass
+from typing import Any
+
+import hypothesis.strategies as st
+import pytest
+from hypothesis import assume, given
+from omegaconf import MISSING, OmegaConf
+
+from hydra_zen import (
+    ZenField,
+    builds,
+    hydrated_dataclass,
+    instantiate,
+    make_config,
+    mutable_value,
+)
+from hydra_zen._compatibility import PATCH_OMEGACONF_830
+from hydra_zen.errors import HydraZenValidationError
+
+
+def test_PATCH_OMEGACONF_830_is_set_properly():
+    # test that PATCH_OMEGACONF_830 is True only if local version
+    # of omegaconf has known bug
+    assert isinstance(PATCH_OMEGACONF_830, bool)
+
+    @dataclass
+    class BasicConf:
+        setup: Any = 1
+
+    @dataclass
+    class Config(BasicConf):
+        setup: Any = field(default_factory=lambda: list(["hi"]))
+
+    conf = OmegaConf.structured(Config)
+    if PATCH_OMEGACONF_830:
+        assert conf.setup == 1
+    else:
+        # local version of omegaconf should have correct behavior
+        assert conf.setup == ["hi"]
+
+
+@dataclass
+class A_inheritance:
+    x: Any
+    y: Any = 1
+
+
+valid_defaults = (
+    st.none()
+    | st.booleans()
+    | st.text(alphabet="abcde")
+    | st.integers(-3, 3)
+    | st.lists(st.integers(-2, 2))
+    | st.fixed_dictionaries({"a": st.integers(-2, 2)})
+)
+
+
+def via_hydrated(x, Parent):
+    z = x if not isinstance(x, (list, dict)) else mutable_value(x)
+
+    @hydrated_dataclass(A_inheritance)
+    class Conf(Parent):
+        x: Any = z
+
+    return Conf
+
+
+def mutable_if_needed(x):
+    if isinstance(x, (dict, list)):
+        return mutable_value(x)
+    return x
+
+
+@pytest.mark.parametrize(
+    "config_maker",
+    [
+        lambda x, Parent: make_config(x=x, bases=(Parent,)),
+        lambda x, Parent: make_config(x=ZenField(Any, x), bases=(Parent,)),
+        lambda x, Parent: make_config(ZenField(Any, x, "x"), bases=(Parent,)),
+        lambda x, Parent: builds(A_inheritance, x=x, builds_bases=(Parent,)),
+        # TODO: add case where x is populated via pop-full-sig
+        # we currently support specifing fields-as-args in builds
+        lambda x, Parent: builds(
+            A_inheritance, x=mutable_if_needed(x), builds_bases=(Parent,)
+        ),
+        via_hydrated,
+    ],
+)
+@given(
+    parent_field_name=st.sampled_from(["x", "y"]),
+    parent_default=valid_defaults,
+    child_default=valid_defaults,
+)
+def test_known_inheritance_issues_in_omegaconf_are_circumvented(
+    parent_field_name, parent_default, child_default, config_maker
+):
+    # Exercises omegaconf bug documented in https://github.com/omry/omegaconf/issues/830
+    # Should pass either because:
+    #  - the test env is running a new version of omegaconf, which has patched this
+    #  - hydra-zen is providing a workaround
+    if PATCH_OMEGACONF_830 and config_maker is via_hydrated:
+        pytest.skip("hydrated_dataclass cannot support patched workaround")
+
+    assume(parent_field_name != "y" or parent_default is not MISSING)
+
+    Parent = make_config(**{parent_field_name: parent_default})
+    Child = config_maker(x=child_default, Parent=Parent)
+
+    if (
+        PATCH_OMEGACONF_830
+        and isinstance(child_default, (list, dict))
+        and not isinstance(parent_default, (list, dict))
+        and parent_field_name == "x"  # parent field overlaps
+    ):
+        # ensure we only case to dataclass when necessary
+        assert is_dataclass(Child.x)
+    else:
+        assert Child().x == child_default
+
+    Obj = instantiate(Child)
+    assert Obj.x == child_default
+
+    if parent_field_name == "y":
+        assert Obj.y == parent_default
+
+
+@pytest.mark.skipif(
+    not PATCH_OMEGACONF_830, reason="issue has been patched by omegaconf"
+)
+def test_hydrated_dataclass_raises_on_omegaconf_inheritance_issue():
+
+    Parent = make_config(x=1)
+
+    with pytest.raises(HydraZenValidationError):
+
+        @hydrated_dataclass(A_inheritance)
+        class Conf(Parent):
+            x: Any = mutable_value([1, 2])
diff --git a/tests/test_config_value_validation.py b/tests/test_config_value_validation.py
index 7e92e0d1..21dd0af8 100644
--- a/tests/test_config_value_validation.py
+++ b/tests/test_config_value_validation.py
@@ -18,10 +18,10 @@ from hydra_zen import (
     make_config,
     to_yaml,
 )
+from hydra_zen._compatibility import ZEN_SUPPORTED_PRIMITIVES
 from hydra_zen.errors import HydraZenUnsupportedPrimitiveError
 from hydra_zen.structured_configs._implementations import HYDRA_SUPPORTED_PRIMITIVES
 from hydra_zen.structured_configs._utils import KNOWN_MUTABLE_TYPES
-from hydra_zen.structured_configs._value_conversion import ZEN_SUPPORTED_PRIMITIVES
 from tests import everything_except
 
 
@@ -109,14 +109,12 @@ construction_fn_variations = [
 @example(unsupported={unsupported_instance: 1})
 @example(unsupported={1: unsupported_instance})
 @example(unsupported={unsupported_instance})
-@example(unsupported={unsupported_instance})
 @example(unsupported=unsupported_subclass)
 @example(unsupported=[unsupported_subclass])
 @example(unsupported=(unsupported_subclass,))
 @example(unsupported={unsupported_subclass: 1})
 @example(unsupported={1: unsupported_subclass})
 @example(unsupported={unsupported_subclass})
-@example(unsupported={unsupported_subclass})
 # Hydra doesn't support dataclass nodes for keys; ensure
 # hydra-zen doesn't provide enhanced primitive support for keys
 @example(unsupported={make_dataclass(1): 1})
diff --git a/tests/test_hydra_behaviors.py b/tests/test_hydra_behaviors.py
index 1bc225f6..9ed655d7 100644
--- a/tests/test_hydra_behaviors.py
+++ b/tests/test_hydra_behaviors.py
@@ -1,25 +1,17 @@
 # Copyright (c) 2021 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
 
-from dataclasses import dataclass, field, is_dataclass
+from dataclasses import dataclass
 from typing import Any, List, Tuple
 
 import hypothesis.strategies as st
 import pytest
-from hypothesis import assume, given
-from omegaconf import MISSING, DictConfig, ListConfig, OmegaConf
+from hypothesis import given
+from omegaconf import DictConfig, ListConfig
 from omegaconf.errors import ValidationError
 
-from hydra_zen import (
-    ZenField,
-    builds,
-    hydrated_dataclass,
-    instantiate,
-    make_config,
-    mutable_value,
-)
-from hydra_zen.errors import HydraZenValidationError
-from hydra_zen.structured_configs._utils import PATCH_OMEGACONF_830, get_obj_path
+from hydra_zen import builds, hydrated_dataclass, instantiate, mutable_value
+from hydra_zen.structured_configs._utils import get_obj_path
 
 
 def f_three_vars(x, y, z):
@@ -210,123 +202,3 @@ def test_type_checking():
 
     # should be ok
     instantiate(builds(g2, x=[conf_C, conf_C]))
-
-
-def test_PATCH_OMEGACONF_830_is_set_properly():
-    # test that PATCH_OMEGACONF_830 is True only if local version
-    # of omegaconf has known bug
-    assert isinstance(PATCH_OMEGACONF_830, bool)
-
-    @dataclass
-    class BasicConf:
-        setup: Any = 1
-
-    @dataclass
-    class Config(BasicConf):
-        setup: Any = field(default_factory=lambda: list(["hi"]))
-
-    conf = OmegaConf.structured(Config)
-    if PATCH_OMEGACONF_830:
-        assert conf.setup == 1
-    else:
-        # local version of omegaconf should have correct behavior
-        assert conf.setup == ["hi"]
-
-
-@dataclass
-class A_inheritance:
-    x: Any
-    y: Any = 1
-
-
-valid_defaults = (
-    st.none()
-    | st.booleans()
-    | st.text(alphabet="abcde")
-    | st.integers(-3, 3)
-    | st.lists(st.integers(-2, 2))
-    | st.fixed_dictionaries({"a": st.integers(-2, 2)})
-)
-
-
-def via_hydrated(x, Parent):
-    z = x if not isinstance(x, (list, dict)) else mutable_value(x)
-
-    @hydrated_dataclass(A_inheritance)
-    class Conf(Parent):
-        x: Any = z
-
-    return Conf
-
-
-def mutable_if_needed(x):
-    if isinstance(x, (dict, list)):
-        return mutable_value(x)
-    return x
-
-
-@pytest.mark.parametrize(
-    "config_maker",
-    [
-        lambda x, Parent: make_config(x=x, bases=(Parent,)),
-        lambda x, Parent: make_config(x=ZenField(Any, x), bases=(Parent,)),
-        lambda x, Parent: make_config(ZenField(Any, x, "x"), bases=(Parent,)),
-        lambda x, Parent: builds(A_inheritance, x=x, builds_bases=(Parent,)),
-        # TODO: add case where x is populated via pop-full-sig
-        # we currently support specifing fields-as-args in builds
-        lambda x, Parent: builds(
-            A_inheritance, x=mutable_if_needed(x), builds_bases=(Parent,)
-        ),
-        via_hydrated,
-    ],
-)
-@given(
-    parent_field_name=st.sampled_from(["x", "y"]),
-    parent_default=valid_defaults,
-    child_default=valid_defaults,
-)
-def test_known_inheritance_issues_in_omegaconf_are_circumvented(
-    parent_field_name, parent_default, child_default, config_maker
-):
-    # Exercises omegaconf bug documented in https://github.com/omry/omegaconf/issues/830
-    # Should pass either because:
-    #  - the test env is running a new version of omegaconf, which has patched this
-    #  - hydra-zen is providing a workaround
-    if PATCH_OMEGACONF_830 and config_maker is via_hydrated:
-        pytest.skip("hydrated_dataclass cannot support patched workaround")
-
-    assume(parent_field_name != "y" or parent_default is not MISSING)
-
-    Parent = make_config(**{parent_field_name: parent_default})
-    Child = config_maker(x=child_default, Parent=Parent)
-
-    if (
-        PATCH_OMEGACONF_830
-        and isinstance(child_default, (list, dict))
-        and not isinstance(parent_default, (list, dict))
-        and parent_field_name == "x"  # parent field overlaps
-    ):
-        # ensure we only case to dataclass when necessary
-        assert is_dataclass(Child.x)
-    else:
-        assert Child().x == child_default
-
-    Obj = instantiate(Child)
-    assert Obj.x == child_default
-
-    if parent_field_name == "y":
-        assert Obj.y == parent_default
-
-
-@pytest.mark.skipif(
-    not PATCH_OMEGACONF_830, reason="issue has been patched by omegaconf"
-)
-def test_hydrated_dataclass_raises_on_omegaconf_inheritance_issue():
-
-    Parent = make_config(x=1)
-
-    with pytest.raises(HydraZenValidationError):
-
-        @hydrated_dataclass(A_inheritance)
-        class Conf(Parent):
-            x: Any = mutable_value([1, 2])
diff --git a/tests/test_protocols.py b/tests/test_protocols.py
index 4c9c1c5e..abcc04cf 100644
--- a/tests/test_protocols.py
+++ b/tests/test_protocols.py
@@ -7,7 +7,7 @@ from typing import Any
 
 import pytest
 
-from hydra_zen import builds, instantiate, just
+from hydra_zen import builds, instantiate, just, make_custom_builds_fn
 from hydra_zen.funcs import get_obj, zen_processing
 from hydra_zen.structured_configs._implementations import (
     is_builds,
@@ -33,7 +33,7 @@ def test_runtime_checkability_of_protocols(fn, protocol):
     assert isinstance(conf, protocol)
 
 
-def test_Builds_is_not_PartialBuilds():
+def test_Builds_is_not_ZenPartialBuilds():
     Conf = builds(dict)
     assert not isinstance(Conf, PartialBuilds)
 
@@ -60,7 +60,10 @@ def test_targeted_dataclass_is_Builds():
     "fn,protocol",
     [
         (just, Just),
-        (partial(builds, zen_partial=True), PartialBuilds),
+        (
+            make_custom_builds_fn(zen_partial=True, zen_meta=dict(_y=None)),
+            PartialBuilds,
+        ),
     ],
 )
 def test_protocol_target_is_correct(fn, protocol):
@@ -79,7 +82,7 @@ class AJust:
 
 
 @dataclass
-class APartial:
+class AZenPartial:
     _target_: Any = zen_processing
     _zen_target: Any = "builtins.int"
     _zen_partial: bool = True
@@ -92,21 +95,21 @@ class NotJust:
 
 
 @dataclass
-class NotPartial:
+class NotZenPartial:
     _target_: Any = "builtins.dict"  # wrong target
     _zen_target: Any = "builtins.int"
     _zen_partial: bool = True
 
 
 @dataclass
-class NotPartial2:
+class NotZenPartial2:
     _target_: Any = "builtins.dict"
     # MISSING _zen_target
     _zen_partial: bool = True
 
 
 @dataclass
-class NotPartial3:
+class NotZenPartial3:
     # partial is False
     _target_: Any = zen_processing
     _zen_target: Any = "builtins.int"
@@ -121,11 +124,12 @@ class NotPartial3:
         (just(int), True, True, False),
         (AJust, True, True, False),
         (builds(int, zen_partial=True), True, False, True),
-        (APartial, True, False, True),
+        (builds(int, zen_partial=True, zen_meta=dict(a=1)), True, False, True),
+        (AZenPartial, True, False, True),
         (NotJust, True, False, False),
-        (NotPartial, True, False, False),
-        (NotPartial2, True, False, False),
-        (NotPartial3, True, False, False),
+        (NotZenPartial, True, False, False),
+        (NotZenPartial2, True, False, False),
+        (NotZenPartial3, True, False, False),
     ],
 )
 def test_protocol_checkers(x, yes_builds, yes_just, yes_partial):
diff --git a/tests/test_roundtrips.py b/tests/test_roundtrips.py
index 5a85de38..0bada6c7 100644
--- a/tests/test_roundtrips.py
+++ b/tests/test_roundtrips.py
@@ -174,6 +174,7 @@ def test_just_roundtrip(obj):
         just,
         lambda x: builds(x, zen_partial=True),
         lambda x: builds(x, zen_meta=dict(_some_obscure_name=1)),
+        lambda x: builds(x, zen_partial=True, zen_meta=dict(_some_obscure_name=1)),
     ],
 )
 def test_get_target_roundtrip(x, fn):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ccf6f97c..d3762a15 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -112,6 +112,7 @@ NoneType = type(None)
         (bool, bool),
         (Color, Color),
         (C, Any),  # unsupported primitives
+        (type(None), Any),
         (set, Any),
         (list, Any),
         (tuple, Any),
diff --git a/tests/test_value_conversion.py b/tests/test_value_conversion.py
index 8a3b83c5..2f10ee24 100644
--- a/tests/test_value_conversion.py
+++ b/tests/test_value_conversion.py
@@ -11,10 +11,8 @@ from hypothesis import HealthCheck, assume, given, settings
 from omegaconf import DictConfig, ListConfig, OmegaConf
 
 from hydra_zen import builds, instantiate, make_config, to_yaml
-from hydra_zen.structured_configs._value_conversion import (
-    ZEN_SUPPORTED_PRIMITIVES,
-    ZEN_VALUE_CONVERSION,
-)
+from hydra_zen._compatibility import ZEN_SUPPORTED_PRIMITIVES
+from hydra_zen.structured_configs._value_conversion import ZEN_VALUE_CONVERSION
 
 
 def test_supported_primitives_in_sync_with_value_conversion():