Repository: mit-ll-responsible-ai/hydra-zen
Pull request #235 (2022-02-26 15:47:09)

Revise implementation of `PartialBuilds`
In light of #230 this PR changes implementation of `PartialBuilds` to reflect that `builds(<target>, zen_partial=True)` can implement either the `_partial_` protocol or the `_zen_partial` protocol. Accordingly, `hydra_zen.typing` now exposes two protocols: `HydraPartialBuilds` and `ZenPartialBuilds`, and `PartialBuilds` is their union.

Any libraries that leverage `PartialBuilds` in their type annotations will likely not see any breaking changes in their static analysis. However, code that used `PartialBuilds` to perform runtime checking will need to update their code:

e.g., from

```python
>>> from hydra_zen.typing import PartialBuilds

>>> Conf = builds(int, zen_partial=True)
>>> isinstance(Conf, PartialBuilds)
True
```

to

```python
>>> from hydra_zen.typing import HydraPartialBuilds, ZenPartialBuilds

>>> Conf = builds(int, zen_partial=True)
>>> isinstance(Conf, (HydraPartialBuilds, ZenPartialBuilds))
True
```

In the future (certainly not until after Hydra 1.1.2 is released), the annotations of `builds` may be revised to more precisely reflect when `HydraPartialBuilds` vs `ZenPartialBuilds` is returned.


Patch:
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index c61707b4..3b40e88b 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -37,7 +37,7 @@ Improvements
 Support for Upcoming Hydra Features
 -----------------------------------
 
-Hydra 1.1.2 is introducing `support for partial instantiation of targeted configs <https://hydra.cc/docs/next/advanced/instantiate_objects/overview/#partial-instantiation>`_ via the ``_partial_`` field. ``builds(<target>, zen_partial=True)`` will now set the ``_partial_`` field on the structured config
+Hydra 1.1.2 will introduce `support for partial instantiation of targeted configs <https://hydra.cc/docs/next/advanced/instantiate_objects/overview/#partial-instantiation>`_ via the ``_partial_`` field. ``builds(<target>, zen_partial=True)`` will now set the ``_partial_`` field on the structured config
 rather than using ``hydra_zen.funcs.zen_processing`` to facilitate partial instantiation.
 
 
@@ -60,8 +60,29 @@ rather than using ``hydra_zen.funcs.zen_processing`` to facilitate partial insta
 
 This change will only occur when one's locally-installed version of ``hydra-core`` is 1.1.2 or higher. Structured configs and yamls that configure partial'd objects via ``hydra_zen.funcs.zen_processing`` are still valid and will instantiate in the same way as before. I.e. this is only a compatibility-breaking change for code that relied on the specific implementation details of the structured config produced by ``builds(<target>, zen_partial=True)``.
 
+In accordance with this change, the definition of ``hydra_zen.typing.PartialBuilds`` has been changed; it now reflects a union of protocols: ``ZenPartialBuilds[T] | HydraPartialBuilds[T]]``, both are which are now part of the public API of ``hydra_zen.typing``.
+
 (See :pull:`186` and :pull:`230` for additional details)
 
+Compatibility-Breaking Changes
+------------------------------
+
+``hydra_zen.typing.PartialBuilds`` is no longer a runtime-checkable protocol.
+Code that used ``PartialBuilds`` in this way can be updated as follows:
+
+
++---------------------------------------------------+--------------------------------------------------------------------------+
+|                                                   |                                                                          |
+| .. code-block:: pycon                             | .. code-block:: pycon                                                    |
+|    :caption: hydra-zen < 0.6.0                    |    :caption: 0.6.0 <= hydra-zen                                          |
+|                                                   |                                                                          |
+|    >>> from hydra_zen.typing import PartialBuilds |    >>> from hydra_zen.typing import HydraPartialBuilds, ZenPartialBuilds |
+|                                                   |                                                                          |
+|    >>> Conf = builds(int, zen_partial=True)       |    >>> Conf = builds(int, zen_partial=True)                              |
+|    >>> isinstance(Conf, PartialBuilds)            |    >>> isinstance(Conf, (HydraPartialBuilds, ZenPartialBuilds))          |
+|    True                                           |    True                                                                  |
++---------------------------------------------------+--------------------------------------------------------------------------+
+
 .. _v0.5.0:
 
 ------------------
diff --git a/docs/source/conf.py b/docs/source/conf.py
index ef369c0a..ff2ec24f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -43,7 +43,7 @@ extensions = [
     "numpydoc",
     "sphinx_copybutton",
     "sphinx_tabs.tabs",
-    # "sphinx_codeautolink",
+    "sphinx_codeautolink",
 ]
 
 # Strip input prompts:
diff --git a/src/hydra_zen/typing/__init__.py b/src/hydra_zen/typing/__init__.py
index 24824dac..b6fd8105 100644
--- a/src/hydra_zen/typing/__init__.py
+++ b/src/hydra_zen/typing/__init__.py
@@ -3,11 +3,13 @@
 
 from ._implementations import (
     Builds,
+    HydraPartialBuilds,
     Importable,
     Just,
     Partial,
     PartialBuilds,
     SupportedPrimitive,
+    ZenPartialBuilds,
     ZenWrappers,
 )
 
@@ -19,4 +21,6 @@ __all__ = [
     "PartialBuilds",
     "SupportedPrimitive",
     "ZenWrappers",
+    "ZenPartialBuilds",
+    "HydraPartialBuilds",
 ]
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 22f505f8..d86cbcad 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -33,6 +33,8 @@ __all__ = [
     "Importable",
     "SupportedPrimitive",
     "ZenWrappers",
+    "ZenPartialBuilds",
+    "HydraPartialBuilds",
 ]
 
 P = ParamSpec("P")
@@ -116,7 +118,7 @@ class Just(Builds[T], Protocol[T]):  # pragma: no cover
 
 
 @runtime_checkable
-class PartialBuilds(Builds[T], Protocol[T]):  # pragma: no cover
+class ZenPartialBuilds(Builds[T], Protocol[T]):  # pragma: no cover
     _target_: ClassVar[
         Literal["hydra_zen.funcs.zen_processing"]
     ] = "hydra_zen.funcs.zen_processing"
@@ -129,6 +131,9 @@ class HydraPartialBuilds(Builds[T], Protocol[T]):  # pragma: no cover
     _partial_: ClassVar[Literal[True]] = True
 
 
+PartialBuilds = Union[ZenPartialBuilds[T], HydraPartialBuilds[T]]
+
+
 @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 d197ad5a..d5f6bf90 100644
--- a/tests/annotations/declarations.py
+++ b/tests/annotations/declarations.py
@@ -32,7 +32,13 @@ from hydra_zen import (
     make_custom_builds_fn,
     mutable_value,
 )
-from hydra_zen.typing import Builds, Partial, PartialBuilds
+from hydra_zen.typing import (
+    Builds,
+    HydraPartialBuilds,
+    Partial,
+    PartialBuilds,
+    ZenPartialBuilds,
+)
 from hydra_zen.typing._builds_overloads import full_builds, partial_builds
 from hydra_zen.typing._implementations import HydraPartialBuilds
 
@@ -53,9 +59,10 @@ def requires_A(x: int):
 
 
 # test type behaviors
-def f1():
+def check_partial_builds_on_class():
     reveal_type(
-        builds(A, zen_partial=True), expected_text="Type[PartialBuilds[Type[A]]]"
+        builds(A, zen_partial=True),
+        expected_text="Type[ZenPartialBuilds[Type[A]]] | Type[HydraPartialBuilds[Type[A]]]",
     )
     conf_a_partial = builds(A, zen_partial=True)
     reveal_type(instantiate(conf_a_partial), expected_text="Partial[A]")
@@ -65,15 +72,18 @@ def f1():
 f_sig = Callable[[int], int]
 
 
-def f2():
+def check_partial_builds_on_function():
     reveal_type(
         builds(f, zen_partial=True),
-        expected_text="Type[PartialBuilds[(x: int) -> int]]",
+        expected_text="Type[ZenPartialBuilds[(x: int) -> int]] | Type[HydraPartialBuilds[(x: int) -> int]]",
     )
 
     conf_f_partial = builds(f, zen_partial=True)
 
-    reveal_type(conf_f_partial(), expected_text="PartialBuilds[(x: int) -> int]")
+    reveal_type(
+        conf_f_partial(),
+        expected_text="ZenPartialBuilds[(x: int) -> int] | HydraPartialBuilds[(x: int) -> int]",
+    )
 
     conf_f_partial_instance = conf_f_partial()
     reveal_type(instantiate(conf_f_partial), expected_text="Partial[int]")
@@ -181,27 +191,27 @@ def zen_wrappers():
 
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=f),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=J),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=B),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=PB),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=(None,)),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
     reveal_type(
         builds(str, zen_partial=True, zen_wrappers=(f, J, B, PB, None)),
-        expected_text="Type[PartialBuilds[Type[str]]]",
+        expected_text="Type[ZenPartialBuilds[Type[str]]] | Type[HydraPartialBuilds[Type[str]]]",
     )
 
     # should fail
@@ -214,7 +224,8 @@ def custom_builds_fn():
 
     reveal_type(_builds(int), expected_text="Type[Builds[Type[int]]]")
     reveal_type(
-        _builds(int, zen_partial=True), expected_text="Type[PartialBuilds[Type[int]]]"
+        _builds(int, zen_partial=True),
+        expected_text="Type[ZenPartialBuilds[Type[int]]] | Type[HydraPartialBuilds[Type[int]]]",
     )
 
 
@@ -295,7 +306,7 @@ def supported_primitives():
             e=f,
             zen_partial=True,
         ),
-        expected_text="Type[PartialBuilds[Type[dict[Unknown, Unknown]]]]",
+        expected_text="Type[ZenPartialBuilds[Type[dict[Unknown, Unknown]]]] | Type[HydraPartialBuilds[Type[dict[Unknown, Unknown]]]]",
     )
 
     # check lists
@@ -414,7 +425,7 @@ def check_partiald_target():
     reveal_type(builds(partial(int)), expected_text="Type[Builds[partial[int]]]")
     reveal_type(
         builds(partial(int), zen_partial=True),
-        expected_text="Type[PartialBuilds[partial[int]]]",
+        expected_text="Type[ZenPartialBuilds[partial[int]]] | Type[HydraPartialBuilds[partial[int]]]",
     )
     a = builds(partial(int))
     reveal_type(instantiate(a), expected_text="int")
@@ -435,10 +446,17 @@ def check_target_annotation():
 
 
 def check_protocols():
+    def f() -> Type[ZenPartialBuilds[Type[int]]]:
+        ...
+
+    def g() -> Type[HydraPartialBuilds[Type[int]]]:
+        ...
+
     reveal_type(builds(int)._target_, expected_text="str")
     reveal_type(builds(int)()._target_, expected_text="str")
 
-    PBuilds = builds(int, zen_partial=True)
+    PBuilds = f()
+
     reveal_type(
         PBuilds._target_, expected_text="Literal['hydra_zen.funcs.zen_processing']"
     )
@@ -452,6 +470,10 @@ def check_protocols():
     reveal_type(PBuilds._zen_partial, expected_text="Literal[True]")
     reveal_type(PBuilds()._zen_partial, expected_text="Literal[True]")
 
+    HPBuilds = g()
+    reveal_type(HPBuilds._partial_, expected_text="Literal[True]")
+    reveal_type(HPBuilds()._partial_, expected_text="Literal[True]")
+
     Just = just(int)
     reveal_type(Just._target_, expected_text="Literal['hydra_zen.funcs.get_obj']")
     reveal_type(Just()._target_, expected_text="Literal['hydra_zen.funcs.get_obj']")
@@ -519,7 +541,8 @@ def check_populate_full_sig():
     Conf_f_partial = builds(f, populate_full_signature=True, zen_partial=True)
     conf2 = Conf_f_partial(not_a_valid_arg=1)  # should be ok
     reveal_type(
-        conf2, expected_text="PartialBuilds[(x: int, y: str, z: bool = False) -> C]"
+        conf2,
+        expected_text="ZenPartialBuilds[(x: int, y: str, z: bool = False) -> C] | HydraPartialBuilds[(x: int, y: str, z: bool = False) -> C]",
     )
 
     # specifying `populate_full_signature=False` should disable sig-reflection
@@ -565,7 +588,7 @@ def check_full_builds():
     Conf_f3 = full_builds(f, zen_partial=True)
     reveal_type(
         Conf_f3,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
     )
     Conf_f3()
 
@@ -578,14 +601,14 @@ def check_partial_builds():
     Conf_f = partial_builds(f)
     reveal_type(
         Conf_f,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
     )
 
     # type-checker should see default: `populate_full_signature=True`
     Conf_f2 = partial_builds(f, zen_partial=True)
     reveal_type(
         Conf_f2,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> Literal[1]]]",
     )
 
     # signature should be required
@@ -646,7 +669,7 @@ def check_make_custom_builds_partial():
 
     reveal_type(
         Conf,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> int]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
     )
 
     partial_builds2 = make_custom_builds_fn(
@@ -657,7 +680,7 @@ def check_make_custom_builds_partial():
 
     reveal_type(
         Conf2,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> int]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
     )
 
     Parent = make_config(x=1)
@@ -670,7 +693,7 @@ def check_make_custom_builds_partial():
 
     reveal_type(
         Conf3,
-        expected_text="Type[PartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
+        expected_text="Type[ZenPartialBuilds[(x: int, y: str, z: bool = False) -> int]] | Type[HydraPartialBuilds[(x: int, y: str, z: bool = False) -> int]]",
     )
 
 
@@ -678,7 +701,7 @@ def check_protocol_compatibility():
     def f_builds(x: Type[Builds]):
         pass
 
-    def f_partial(x: Type[PartialBuilds]):
+    def f_partial(x: Type[PartialBuilds[Any]]):
         pass
 
     def f(x: int):
diff --git a/tests/test_compatibility/test_hydra_supports_partial.py b/tests/test_compatibility/test_hydra_supports_partial.py
index 08dd19ce..a08f42b3 100644
--- a/tests/test_compatibility/test_hydra_supports_partial.py
+++ b/tests/test_compatibility/test_hydra_supports_partial.py
@@ -11,8 +11,7 @@ from hydra_zen.structured_configs._implementations import (
     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
+from hydra_zen.typing import HydraPartialBuilds, ZenPartialBuilds
 
 
 @dataclass
@@ -32,7 +31,7 @@ class ZenPartialConf:
 
 def test_HydraPartialBuilds_protocol():
     assert isinstance(HydraPartialConf(), HydraPartialBuilds)
-    assert not isinstance(HydraPartialConf(), PartialBuilds)
+    assert not isinstance(HydraPartialConf(), ZenPartialBuilds)
 
 
 def test_HYDRA_SUPPORTS_PARTIAL_is_set_properly():
diff --git a/tests/test_protocols.py b/tests/test_protocols.py
index 2e9d2b10..22007d1a 100644
--- a/tests/test_protocols.py
+++ b/tests/test_protocols.py
@@ -14,7 +14,7 @@ from hydra_zen.structured_configs._implementations import (
     is_just,
     is_partial_builds,
 )
-from hydra_zen.typing import Builds, Just, Partial, PartialBuilds
+from hydra_zen.typing import Builds, Just, Partial, ZenPartialBuilds
 from hydra_zen.typing._implementations import HydraPartialBuilds
 
 
@@ -23,8 +23,8 @@ from hydra_zen.typing._implementations import HydraPartialBuilds
     [
         (just, Just),
         (builds, Builds),
-        (partial(builds, zen_partial=True), (PartialBuilds, HydraPartialBuilds)),
-        (partial(builds, zen_partial=True, zen_meta=dict(y=1)), PartialBuilds),
+        (partial(builds, zen_partial=True), (ZenPartialBuilds, HydraPartialBuilds)),
+        (partial(builds, zen_partial=True, zen_meta=dict(y=1)), ZenPartialBuilds),
     ],
 )
 def test_runtime_checkability_of_protocols(fn, protocol):
@@ -37,7 +37,7 @@ def test_runtime_checkability_of_protocols(fn, protocol):
 
 def test_Builds_is_not_ZenPartialBuilds():
     Conf = builds(dict)
-    assert not isinstance(Conf, PartialBuilds)
+    assert not isinstance(Conf, ZenPartialBuilds)
 
     PConf = builds(dict, zen_partial=True)
     assert isinstance(PConf, Builds)
@@ -64,7 +64,7 @@ def test_targeted_dataclass_is_Builds():
         (just, Just),
         (
             make_custom_builds_fn(zen_partial=True, zen_meta=dict(_y=None)),
-            PartialBuilds,
+            ZenPartialBuilds,
         ),
     ],
 )