Repository: mit-ll-responsible-ai/hydra-zen
Pull request #276 (2022-05-18 17:11:28)

Defer to Hydra support for `pathlib.Path`
- Defer to Hydra support for `pathlib.Path` when appropriate
- Also bypasses hydra-zen fix for https://github.com/omry/omegaconf/issues/830 when appropriate.
- Fixes failing tests caused by release of hydra 1.2.0


Patch:
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index 2132a8dd..9c04e6dd 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -117,6 +117,7 @@ Values of the following types can be specified directly in configs:
 - :py:class:`dict`
 - :py:class:`enum.Enum`
 - :py:class:`bytes`  (*support added in OmegaConf 2.2.0*)
+- :py:class:`pathlib.Path`  (*support added in OmegaConf 2.2.1*)
 
 .. _additional-types:
 
@@ -153,7 +154,7 @@ hydra-zen provides specialized support for values of the following types:
 - :py:class:`collections.Counter`
 - :py:class:`collections.deque`
 - :py:func:`functools.partial`
-- :py:class:`pathlib.Path`
+- :py:class:`pathlib.Path` (*support provided for OmegaConf < 2.2.1*)
 - :py:class:`pathlib.PosixPath`
 - :py:class:`pathlib.WindowsPath`
 - :py:class:`range`
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index 58797a2b..7d87e239 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -8,6 +8,24 @@ Changelog
 This is a record of all past hydra-zen releases and what went into them, in reverse 
 chronological order. All previous releases should still be available on pip.
 
+.. _v0.8.0:
+
+------------------
+0.8.0 - 2022-XX-XX
+------------------
+
+.. note:: This is documentation for an unreleased version of hydra-zen
+
+
+Support for New Hydra/OmegaConf Features
+----------------------------------------
+OmegaConf ``v2.2.1`` is added native support for the following types:
+
+- :py:class:`pathlib.Path`
+
+hydra-zen :ref:`already provides support for these <additional-types>`, but this version will defer to OmegaConf's native support when possible. (See :pull:`276`)
+
+
 .. _v0.7.0:
 
 ------------------
@@ -33,8 +51,8 @@ values will automatically be converted to structured configs.
    >>> from functools import partial
    >>> from hydra_zen import to_yaml, just
 
-   >>> def f(x): returns x**2
-   >>> partiald_f = partial(f, x=2))
+   >>> def f(x): return x**2
+   >>> partiald_f = partial(f, x=2)
 
    >>> just(partiald_f)  # convert to structured config
    PartialBuilds_f(_target_='__main__.f', _partial_=True, x=2)
@@ -51,7 +69,6 @@ Support for Upcoming Hydra/OmegaConf Features
 OmegaConf ``v2.2.0`` is adding native support for the following types:
 
 - :py:class:`bytes`
-- :py:class:`pathlib.Path`
 
 hydra-zen :ref:`already provides support for these <additional-types>`, but this version will defer to OmegaConf's native support when possible. (See :pull:`262`)
 
diff --git a/setup.cfg b/setup.cfg
index f89ff04a..b21818ed 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -59,7 +59,7 @@ basepython = python3.8
 
 [testenv:hydra-1p1p2-pre-release]  # test against pre-releases of dependencies
 pip_pre = true
-deps = hydra-core==1.1.2dev
+deps = hydra-core==1.1.2
        {[testenv]deps}
        pydantic
        beartype
@@ -71,7 +71,6 @@ setenv = NUMBA_DISABLE_JIT=1
 usedevelop = true
 basepython = python3.8
 deps = {[testenv]deps}
-       omegaconf==2.1.1  # ensure we cover paths that patch omegaconf 830
        coverage
        pytest-cov
        numpy
diff --git a/src/hydra_zen/_compatibility.py b/src/hydra_zen/_compatibility.py
index fd6d8b9d..4d516982 100644
--- a/src/hydra_zen/_compatibility.py
+++ b/src/hydra_zen/_compatibility.py
@@ -44,7 +44,7 @@ HYDRA_VERSION: Final = _get_version(hydra.__version__)
 #
 # Uncomment dynamic setting once OmegaConf merges fix:
 # https://github.com/omry/omegaconf/pull/832
-PATCH_OMEGACONF_830: Final = True  # OMEGACONF_VERSION < Version(2, 1, 1)
+PATCH_OMEGACONF_830: Final = OMEGACONF_VERSION < Version(2, 2, 1)
 
 # Hydra's instantiate API now supports partial-instantiation, indicated
 # by a `_partial_ = True` attribute.
@@ -53,6 +53,7 @@ HYDRA_SUPPORTS_PARTIAL: Final = Version(1, 1, 1) < HYDRA_VERSION
 
 HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES: Final = OMEGACONF_VERSION >= Version(2, 2, 0)
 HYDRA_SUPPORTS_BYTES: Final = OMEGACONF_VERSION >= Version(2, 2, 0)
+HYDRA_SUPPORTS_Path: Final = OMEGACONF_VERSION >= Version(2, 2, 1)
 
 # Indicates primitive types permitted in type-hints of structured configs
 HYDRA_SUPPORTED_PRIMITIVE_TYPES: Final = {int, float, bool, str, Enum}
@@ -63,9 +64,6 @@ ZEN_SUPPORTED_PRIMITIVES: Set[type] = {
     frozenset,
     complex,
     partial,
-    Path,
-    PosixPath,
-    WindowsPath,
     bytearray,
     deque,
     Counter,
@@ -78,3 +76,13 @@ if HYDRA_SUPPORTS_BYTES:  # pragma: no cover
     HYDRA_SUPPORTED_PRIMITIVE_TYPES.add(bytes)
 else:  # pragma: no cover
     ZEN_SUPPORTED_PRIMITIVES.add(bytes)
+
+_path_types = {Path, PosixPath, WindowsPath}
+
+if HYDRA_SUPPORTS_Path:  # pragma: no cover
+    HYDRA_SUPPORTED_PRIMITIVES.update(_path_types)
+    HYDRA_SUPPORTED_PRIMITIVE_TYPES.add(Path)
+else:  # pragma: no cover
+    ZEN_SUPPORTED_PRIMITIVES.update(_path_types)
+
+del _path_types
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index b08ba5bb..192910c3 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -325,24 +325,20 @@ def hydrated_dataclass(
         decorated_obj = cast(Any, decorated_obj)
         decorated_obj = dataclass(frozen=frozen)(decorated_obj)
 
-        if PATCH_OMEGACONF_830 and 2 < len(decorated_obj.__mro__):
+        if PATCH_OMEGACONF_830 and 2 < len(decorated_obj.__mro__):  # pragma: no cover
             parents = decorated_obj.__mro__[1:-1]
             # this class inherits from a parent
             for field_ in fields(decorated_obj):
                 if field_.default_factory is not MISSING and any(
                     hasattr(p, field_.name) for p in parents
                 ):
-                    # TODO: update error message with fixed omegaconf version
-                    _value = field_.default_factory()
                     raise HydraZenValidationError(
                         "This config will not instantiate properly.\nThis is due to a "
                         "known bug in omegaconf: The config specifies a "
                         f"default-factory for field {field_.name}, and inherits from a "
                         "parent that specifies the same field with a non-factory value "
                         "-- the parent's value will take precedence.\nTo circumvent "
-                        f"this, specify {field_.name} using: "
-                        f"`builds({type(_value).__name__}, {_value})`\n\nFor more "
-                        "information, see: https://github.com/omry/omegaconf/issues/830"
+                        f"this upgrade to omegaconf 2.2.1 or higher."
                     )
 
         if populate_full_signature:
@@ -548,8 +544,8 @@ def sanitized_field(
     ):
         if _mutable_default_permitted:
             return cast(Field[Any], mutable_value(value))
-
-        value = builds(type(value), value)
+        else:  # pragma: no cover
+            value = builds(type(value), value)
 
     return _utils.field(
         default=sanitized_default_value(
@@ -1656,7 +1652,7 @@ def builds(
                 PATCH_OMEGACONF_830
                 and builds_bases
                 and value.default_factory is not MISSING
-            ):
+            ):  # pragma: no cover
 
                 # Addresses omegaconf #830 https://github.com/omry/omegaconf/issues/830
                 #
diff --git a/src/hydra_zen/structured_configs/_just.py b/src/hydra_zen/structured_configs/_just.py
index f355ae68..562f0ca8 100644
--- a/src/hydra_zen/structured_configs/_just.py
+++ b/src/hydra_zen/structured_configs/_just.py
@@ -1,6 +1,5 @@
 # Copyright (c) 2022 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
-from pathlib import Path
 from typing import Any, FrozenSet, Type, TypeVar, Union, overload
 
 from hydra_zen.typing import Builds, Importable, Just
@@ -8,7 +7,7 @@ from hydra_zen.typing._implementations import _HydraPrimitive  # type: ignore
 from hydra_zen.typing._implementations import _SupportedViaBuilds  # type: ignore
 
 from ._implementations import sanitized_default_value
-from ._value_conversion import ConfigComplex, ConfigPath
+from ._value_conversion import ConfigComplex
 
 # pyright: strict
 
@@ -28,11 +27,6 @@ def just(obj: complex) -> ConfigComplex:  # pragma: no cover
     ...
 
 
-@overload
-def just(obj: Path) -> ConfigPath:  # pragma: no cover
-    ...
-
-
 @overload
 def just(obj: TB) -> Builds[Type[TB]]:  # pragma: no cover
     ...
diff --git a/src/hydra_zen/structured_configs/_make_config.py b/src/hydra_zen/structured_configs/_make_config.py
index f52b0ee0..e7a1cd21 100644
--- a/src/hydra_zen/structured_configs/_make_config.py
+++ b/src/hydra_zen/structured_configs/_make_config.py
@@ -92,7 +92,7 @@ def _repack_zenfield(value: ZenField, name: str, bases: Tuple[DataClass_, ...]):
         and not _utils.mutable_default_permitted(bases, field_name=name)
         and isinstance(default, _Field)
         and default.default_factory is not MISSING
-    ):
+    ):  # pragma: no cover
         return ZenField(
             hint=value.hint,
             default=default.default_factory(),
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index 85da45a1..fea56950 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -5,6 +5,7 @@ import sys
 import warnings
 from dataclasses import MISSING, field as _field, is_dataclass
 from enum import Enum
+from pathlib import Path
 from typing import (
     Any,
     Callable,
@@ -29,6 +30,7 @@ from typing_extensions import Final, TypeGuard
 from hydra_zen._compatibility import (
     HYDRA_SUPPORTED_PRIMITIVE_TYPES,
     HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES,
+    HYDRA_SUPPORTS_PARTIAL,
     PATCH_OMEGACONF_830,
 )
 from hydra_zen.errors import HydraZenValidationError
@@ -308,7 +310,7 @@ def sanitized_type(
     no_nested_container = not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES
 
     if origin is not None:
-        if primitive_only:
+        if primitive_only:  # pragma: no cover
             return Any
 
         args = get_args(type_)
@@ -368,19 +370,23 @@ def sanitized_type(
 
         return Any
 
+    if HYDRA_SUPPORTS_PARTIAL and isinstance(type_, type) and issubclass(type_, Path):
+        type_ = Path
+
     if (
         type_ is Any
         or type_ in HYDRA_SUPPORTED_PRIMITIVE_TYPES
         or is_dataclass(type_)
         or (isinstance(type_, type) and issubclass(type_, Enum))
     ):
+
         if wrap_optional and type_ is not Any:  # pragma: no cover
             # normally get_type_hints automatically resolves Optional[...]
             # when None is set as the default, but this has been flaky
             # for some pytorch-lightning classes. So we just do it ourselves...
             # It might be worth removing this later since none of our standard tests
             # cover it.
-            type_ = Optional[type_]
+            type_ = Optional[type_]  # type: ignore
         return type_
 
     # Needed to cover python 3.6 where __origin__ doesn't normalize to type
@@ -429,17 +435,17 @@ def check_suspicious_interpolations(
 
 
 def mutable_default_permitted(bases: Iterable[DataClass_], field_name: str) -> bool:
-    if not PATCH_OMEGACONF_830:  # pragma: no cover
+    if not PATCH_OMEGACONF_830:
+        return True
+    else:  # pragma: no cover
+        for base in bases:
+            if (
+                field_name in base.__dataclass_fields__
+                and base.__dataclass_fields__[field_name].default is not MISSING
+            ):
+                # see https://github.com/omry/omegaconf/issues/830
+                return False
         return True
-
-    for base in bases:
-        if (
-            field_name in base.__dataclass_fields__
-            and base.__dataclass_fields__[field_name].default is not MISSING
-        ):
-            # see https://github.com/omry/omegaconf/issues/830
-            return False
-    return True
 
 
 def valid_defaults_list(hydra_defaults: Any) -> bool:
diff --git a/src/hydra_zen/structured_configs/_value_conversion.py b/src/hydra_zen/structured_configs/_value_conversion.py
index 93e5e01a..1b3d016c 100644
--- a/src/hydra_zen/structured_configs/_value_conversion.py
+++ b/src/hydra_zen/structured_configs/_value_conversion.py
@@ -5,11 +5,13 @@ from dataclasses import dataclass, field
 from pathlib import Path, PosixPath, WindowsPath
 from typing import Any, Callable, Dict, Tuple, Type, cast
 
+from hydra_zen._compatibility import ZEN_SUPPORTED_PRIMITIVES
 from hydra_zen.typing import Builds
 
 from ._utils import get_obj_path
 
 # Some primitive support implemented in _implementations.py
+
 ZEN_VALUE_CONVERSION: Dict[type, Callable[[Any], Any]] = {}
 
 
@@ -33,10 +35,11 @@ class ConfigPath:
     _target_: str = field(default=get_obj_path(Path), init=False)
 
 
-def convert_path(value: Path) -> Builds[Type[Path]]:
-    return cast(Builds[Type[Path]], ConfigPath(_args_=(str(value),)))
+if Path in ZEN_SUPPORTED_PRIMITIVES:  # pragma no cover
 
+    def convert_path(value: Path) -> Builds[Type[Path]]:
+        return cast(Builds[Type[Path]], ConfigPath(_args_=(str(value),)))
 
-ZEN_VALUE_CONVERSION[Path] = convert_path
-ZEN_VALUE_CONVERSION[PosixPath] = convert_path
-ZEN_VALUE_CONVERSION[WindowsPath] = convert_path
+    ZEN_VALUE_CONVERSION[Path] = convert_path
+    ZEN_VALUE_CONVERSION[PosixPath] = convert_path
+    ZEN_VALUE_CONVERSION[WindowsPath] = convert_path
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 665b87c7..857056cb 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -6,7 +6,7 @@
 import sys
 import types
 from enum import Enum
-from pathlib import Path
+from pathlib import Path, PosixPath, WindowsPath
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -186,12 +186,7 @@ class HasTarget(Protocol):  # pragma: no cover
 Importable = TypeVar("Importable", bound=Callable[..., Any])
 
 _HydraPrimitive: TypeAlias = Union[
-    bool,
-    None,
-    int,
-    float,
-    str,
-    ByteString,
+    bool, None, int, float, str, ByteString, Path, WindowsPath, PosixPath
 ]
 
 _SupportedViaBuilds = Union[
@@ -208,7 +203,6 @@ _SupportedPrimitive: TypeAlias = Union[
     Enum,
     DataClass_,
     complex,
-    Path,
     _SupportedViaBuilds,
     EmptyDict,  # not covered by Mapping[..., ...]]
 ]


Test patch:
diff --git a/tests/annotations/declarations.py b/tests/annotations/declarations.py
index 11fe73a9..441ef413 100644
--- a/tests/annotations/declarations.py
+++ b/tests/annotations/declarations.py
@@ -141,7 +141,7 @@ def check_just():
     reveal_type(just("hi"), expected_text="str")
     reveal_type(just(b"1234"), expected_text="bytes")
     reveal_type(just(1 + 2j), expected_text="ConfigComplex")
-    reveal_type(just(Path.home()), expected_text="ConfigPath")
+    reveal_type(just(Path.home()), expected_text="Path")
     reveal_type(just(partial(f, 1)), expected_text="Type[Just[partial[int]]]")
     reveal_type(just(set([1, 2, 3])), expected_text="Builds[Type[set[int]]]")
     reveal_type(just(range(10)), expected_text="Builds[Type[range]]")
diff --git a/tests/test_compatibility/test_primitive_support.py b/tests/test_compatibility/test_primitive_support.py
index 91156ea9..b01dbc8e 100644
--- a/tests/test_compatibility/test_primitive_support.py
+++ b/tests/test_compatibility/test_primitive_support.py
@@ -3,25 +3,44 @@
 
 
 from dataclasses import dataclass
+from pathlib import Path
 
 import pytest
 from omegaconf import OmegaConf, ValidationError
 
 from hydra_zen import builds, instantiate
-from hydra_zen._compatibility import HYDRA_SUPPORTS_BYTES
+from hydra_zen._compatibility import HYDRA_SUPPORTS_BYTES, HYDRA_SUPPORTS_Path
 
 
 @dataclass
-class C:
+class C_bytes:
     x: bytes = b"123"
 
 
+@dataclass
+class C_paths:
+    x: Path = Path.cwd()
+
+
 def test_hydra_supports_bytes():
 
     if HYDRA_SUPPORTS_BYTES:
-        OmegaConf.create(C)
+        OmegaConf.create(C_bytes)  # type: ignore
+    else:
+        with pytest.raises(ValidationError):
+            OmegaConf.create(C_bytes)  # type: ignore
+
+    assert instantiate(builds(C_bytes, populate_full_signature=True)).x == C_bytes.x
+
+
+def test_hydra_supports_paths():
+
+    if HYDRA_SUPPORTS_Path:
+        out = OmegaConf.create(C_paths)  # type: ignore
+        assert isinstance(out.x, Path)
     else:
         with pytest.raises(ValidationError):
-            OmegaConf.create(C)
+            OmegaConf.create(C_paths)  # type: ignore
 
-    assert instantiate(builds(C, populate_full_signature=True)).x == C.x
+    builds_out = instantiate(builds(C_paths, populate_full_signature=True))
+    assert builds_out.x == C_paths.x
diff --git a/tests/test_just.py b/tests/test_just.py
index 1a3dc699..af998106 100644
--- a/tests/test_just.py
+++ b/tests/test_just.py
@@ -2,7 +2,6 @@
 # SPDX-License-Identifier: MIT
 
 import functools
-from pathlib import Path
 
 import pytest
 
@@ -31,7 +30,6 @@ def func_with_cache(x: int):
     [
         bytearray([1, 2, 3]),
         1 + 2j,
-        Path.cwd(),
         A,
         f,
         func_with_cache,
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b4617460..8e840e81 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -6,6 +6,7 @@ import random
 import string
 import sys
 from dataclasses import dataclass, field as dataclass_field
+from pathlib import Path, PosixPath, WindowsPath
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
 
 import hypothesis.strategies as st
@@ -24,6 +25,7 @@ from hydra_zen import builds, instantiate, mutable_value
 from hydra_zen._compatibility import (
     HYDRA_SUPPORTS_BYTES,
     HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES,
+    HYDRA_SUPPORTS_Path,
     Version,
     _get_version,
 )
@@ -117,6 +119,9 @@ NoneType = type(None)
         (str, str),
         (bool, bool),
         (bytes, bytes if HYDRA_SUPPORTS_BYTES else Any),
+        (Path, Path if HYDRA_SUPPORTS_Path else Any),
+        (PosixPath, Path if HYDRA_SUPPORTS_Path else Any),
+        (WindowsPath, Path if HYDRA_SUPPORTS_Path else Any),
         (Color, Color),
         (C, Any),  # unsupported primitives
         (type(None), Any),
@@ -219,7 +224,7 @@ def test_sanitized_type_expected_behavior(in_type, expected_type):
                 ConfigValueError,
             )
         ):
-            Conf = OmegaConf.create(Bad)
+            Conf = OmegaConf.create(Bad)  # type: ignore
             Conf.x = [[int]]  # special case: validate
 
     @dataclass