Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer to Hydra support for pathlib.Path #276

Merged
merged 4 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/source/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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`
Expand Down
23 changes: 20 additions & 3 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

------------------
Expand All @@ -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)
Expand All @@ -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`)

Expand Down
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/hydra_zen/_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _get_version(ver_str: str) -> 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.
Expand All @@ -53,6 +53,7 @@ def _get_version(ver_str: str) -> 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}
Expand All @@ -63,9 +64,6 @@ def _get_version(ver_str: str) -> Version:
frozenset,
complex,
partial,
Path,
PosixPath,
WindowsPath,
bytearray,
deque,
Counter,
Expand All @@ -78,3 +76,13 @@ def _get_version(ver_str: str) -> Version:
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
14 changes: 5 additions & 9 deletions src/hydra_zen/structured_configs/_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,24 +325,20 @@ def wrapper(decorated_obj: Any) -> Any:
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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
#
Expand Down
8 changes: 1 addition & 7 deletions src/hydra_zen/structured_configs/_just.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# 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
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

Expand All @@ -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
...
Expand Down
2 changes: 1 addition & 1 deletion src/hydra_zen/structured_configs/_make_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
30 changes: 18 additions & 12 deletions src/hydra_zen/structured_configs/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import warnings
from dataclasses import MISSING, field as _field, is_dataclass
from enum import Enum
from pathlib import Path
from typing import (
Any,
Callable,
Expand All @@ -29,6 +30,7 @@
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
Expand Down Expand Up @@ -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_)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
13 changes: 8 additions & 5 deletions src/hydra_zen/structured_configs/_value_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
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]] = {}


Expand All @@ -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
10 changes: 2 additions & 8 deletions src/hydra_zen/typing/_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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[
Expand All @@ -208,7 +203,6 @@ class HasTarget(Protocol): # pragma: no cover
Enum,
DataClass_,
complex,
Path,
_SupportedViaBuilds,
EmptyDict, # not covered by Mapping[..., ...]]
]
Expand Down
2 changes: 1 addition & 1 deletion tests/annotations/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class A:
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]]")
Expand Down
Loading