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

Add customizable auto-config support to builds et al #553

Merged
merged 29 commits into from Oct 23, 2023
Merged

Add customizable auto-config support to builds et al #553

merged 29 commits into from Oct 23, 2023

Conversation

rsokl
Copy link
Contributor

@rsokl rsokl commented Oct 19, 2023

This PR introduces a type BuildsFn, which exposes all of the type-refinement and auto-config capabilities that are leverages by builds, just, and make_config. The idea here is that users can now add customized auto-config and type-refinement support for their own types to their own versions of builds, just, etc.

This has been a feature that we have considered adding to hydra-zen for a while, but the issue was that we did not know how to provide type-checking support when users could ostensibly make new types acceptable to, e.g., builds. The way that this is solved is that BuildsFn is a generic, whose type-var describes the types that are accepted by that specific implementation of BuildsFn.

E.g. builds = BuildsFn[SupportedPrimitives].builds reflects the typical builds function. But suppose you want to add custom auto-config support for instances of MyType. You can do:

class CustomBuildsFn(BuildsFn[MyType | SupportedPrimitives]):
    """
    - To customize type-refinement support, override `_sanitized_type`.
    - To customize auto-config support, override `_make_hydra_compatible`.
    - To customize the ability to resolve import paths, override `_get_obj_path`.
    """
    # add support for `MyType` here.

my_builds = CustomBuildsFn.builds
my_just = CustomBuildsFn.just
my_make_config = CustomBuildsFn.make_config

Now you can do

def foo(x: MyType = MyType("blah")): ...

my_builds(foo, populate_full_signature=True)

and my_builds will know how to automatically (and recursively!) convert MyType into a hydra-compatible config!

Defining a Protocol for Automatic Configurablility

Here is an example where we define a protocol that enables a type to describe how to access the kwargs that describe one of its instances. We will create a version of BuildsFn that will check if an object implements this protocol; if it does, the builds/just/make_config functions will know how to "automatically" create a config describing the object.

This is an extremely powerful pattern. It means that 3rd party types can make themselves automatically-configurable in your experiment framework by implementing simply this simple protocol, which you designed. 3rd parties need not modify your implementation of BuildsFn.

from typing import (
    Protocol,
    runtime_checkable,
)

from hydra_zen import BuildsFn
from hydra_zen.typing import CustomConfigType


@runtime_checkable
class AutoConfig(Protocol):
    """Implementing this protocol will enable an object to be passed directly to
    config-creation functions."""
    
    def _hydra_config(self, BldFn: Type[BuildsFn[Any]]) -> Dict[str, Tp]:
        """Return a dict of hydra-zen-compatible kwargs that should be used to recreate this instance from a config.
        """
        ...



class CustomBuilds(BuildsFn[CustomConfigType[AutoConfig]]):
    # you can also do BuildsFn[CustomConfigType[AutoConfig | ConcreteType1 | ConcreteType2]]
    """Exposes config-creation functions that are compatible with objects that implement
        the `AutoConfig` protocol."""
    @classmethod
    def _make_hydra_compatible(
        cls, value: Any, *, structured_conf_permitted: bool = True, **kw
    ) -> HydraSupportedType:
        if structured_conf_permitted and isinstance(value, AutoConfig):
            return cls.builds(type(value), **value._hydra_config(cls))  # type: ignore

        return super()._make_hydra_compatible(
            value,
            structured_conf_permitted=structured_conf_permitted,
            **kw,
        )
       # elif: ConcreteType1 case, ConcreteType2 case, ...

just = CustomBuilds.just
builds = CustomBuilds.builds
make_config = CustomBuilds.make_config

Seeing the config-creation functions in action.

from hydra_zen import to_yaml

class SomeType:
    """Some type that implements the AutoConfig protocol."""
    def __repr__(self) -> str:
        return f"SomeType({self._x!r}, {self._y!r})"

    def __init__(self, x: int, y: Optional["SomeType"] = None) -> None:
        self._x = x
        self._y = y

    def _hydra_config(self, BldFn: Type[BuildsFn[Any]]) -> Dict[str, Tp]:
        # hydra-zen will apply auto-config logic recursively, so there
        # we can return `self._y`, which is of type `SomeType`, as-is.
        return {"x": self._x, "y": self._y}


def pyaml(x): print(to_yaml(x))

>>> pyaml(just(SomeType(1, SomeType(2))))
_target_: __main__.SomeType
x: 1
'y':
  _target_: __main__.SomeType
  x: 2
  'y': null

>>> pyaml(builds(dict, w=[SomeType(8), SomeType(9)]))
_target_: builtins.dict
w:
- _target_: __main__.SomeType
  x: 8
  'y': null
- _target_: __main__.SomeType
  x: 9
  'y': null

>>> pyaml(make_config(z=SomeType(-1)))
z:
  _target_: __main__.SomeType
  x: -1
  'y': null

@rsokl rsokl marked this pull request as ready for review October 23, 2023 16:52
@rsokl rsokl merged commit 3be1c2c into main Oct 23, 2023
16 checks passed
@rsokl rsokl deleted the modularity branch October 23, 2023 17:00
rsokl added a commit that referenced this pull request Oct 25, 2023
Add customizable auto-config support to `builds` et al
@rsokl rsokl added this to the v0.12.0 milestone Oct 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant