Repository: mit-ll-responsible-ai/hydra-zen
Pull request #112 (2021-09-30 00:29:42)

New feature: hydra_meta
See #102 for a more detailed / verbose pitch.

This adds a new zen-processing capability: `hydra_meta`. This enables users to attach "meta" fields to targeted configs, which will *not* be used by `instantiate` to build the target.

In its simplest form:

```python
>>> Conf = builds(dict, a=1, b=2, hydra_meta=dict(c=3))
>>> Conf.a, Conf.b, Conf.c 
(1, 2, 3)
>>> instantiate(Conf)  # notice that c is **not** included in the instantiation
{'a': 1, 'b': 2}
```

Some uses for this:

### 1. Attach a meta field that can be referenced via relative interpolation:

```python
>>> Conf = builds(dict, a="${.s}", b="${.s}", hydra_meta=dict(s=-10))
>>> instantiate(Conf)
{'a': -10, 'b': -10}
>>> instantiate(Conf, s=2)
{'a': 2, 'b': 2}
```

What is powerful about this is that the interpolation is *relative* and *portable*: you can use `Conf` at any nested level in your config, and the interpolation will be valid.

```python
>>> C2 = builds(dict, inner=Conf)
>>> instantiate(C2)
{'inner': {'a': -10, 'b': -10}}

>>> c2 = C2()
>>> c2.inner.s=-100
>>> instantiate(c2)
{'inner': {'a': -100, 'b': -100}}
```

### 2. "Delete" unwanted fields that are being inherited

```python
def f1(x, y): return x, y
def f2(x): return x

Conf1 = builds(f1, x=1, y=2)
```

```python
>>> Conf2_bad = builds(f2, builds_bases=(Conf1,)) 
TypeError: Building: f2 ..
The following unexpected keyword argument(s) for __main__.f2 was specified via inheritance from a base class: y
```

```python
>>> Conf2 = builds(f2, hydra_meta=dict(y=None), builds_bases=(Conf1,))
>>> instantiate(Conf2)
1
```

## Some Implementation Details

Meta-fields cannot occupy any names that would be used to configure the target. Doing so would make the parameter unconfigurable, and would probably be due to a mistake on the user's end.

```python
>>> def f(x): pass
>>> builds(f, hydra_meta=dict(x=1)) 
ValueError: `builds(..., hydra_meta=<...>)`: `hydra_meta` cannot not specify names that exist in the target's signature: x
```

When no signature (or nameable parameters) is present, we still check for collisions

```python
>>> builds(dict, a=1, hydra_meta=dict(a=None))
ValueError: `builds(..., hydra_meta=<...>)`: `hydra_meta` cannot not specify names that are common with those specified in **kwargs_for_target: a
```


Patch:
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index d2ebbdf3..beaf34a2 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -14,9 +14,10 @@ All previous releases should still be available on pip.
 This release:
 
 - Makes the "target" of `builds` a positional-only argument. Code that specifies ``builds(target=<target>, ...)`` will now raise a deprecation warning. Now, e.g., ``builds(dict, target=1)`` will work. `#104 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/104>`_
+- Adds the ability to set "meta" fields via `builds`. See `#112 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/112>`_
 - Adds the :func:`~hydra_zen.get_target` function for retrieving target-objects from structured configs. `#94 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/94>`_
 - Improves type-annotations on `builds`. Now, e.g., ``builds("hi")`` will be marked as invalid by static checkers (the target of `builds` must be callable). `#104 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/104>`_.
-- Migrates zen-specific fields to a new naming-scheme, and zen-specific processing to a universal mechanism. See `#110 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/110>_` for more details.
+- Migrates zen-specific fields to a new naming-scheme, and zen-specific processing to a universal mechanism. See `#110 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/110>`_ for more details.
 - Ensures that hydra-zen's source code is "pyright-clean", under `pyright's basic type-checking mode <https://github.com/microsoft/pyright/blob/main/docs/configuration.md#diagnostic-rule-defaults>`_. `#101 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/101>`_
 - Adds to all public modules/packages an ``__all__`` field. `#99 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/99>`_
 - Adds PEP 561 compliance (e.g. hydra-zen is now compatible with mypy). `#97 <https://github.com/mit-ll-responsible-ai/hydra-zen/pull/97>`_
diff --git a/src/hydra_zen/funcs.py b/src/hydra_zen/funcs.py
index f4fab46c..7055a57f 100644
--- a/src/hydra_zen/funcs.py
+++ b/src/hydra_zen/funcs.py
@@ -40,8 +40,19 @@ def get_obj(*, path: str) -> _typing.Union[type, _typing.Callable[..., _typing.A
         raise e
 
 
-def zen_processing(*args, _zen_target: str, _zen_partial: bool = False, **kwargs):
+def zen_processing(
+    *args,
+    _zen_target: str,
+    _zen_partial: bool = False,
+    _zen_exclude: _typing.Sequence[str] = tuple(),
+    **kwargs,
+):
     obj = get_obj(path=_zen_target)
+
+    if _zen_exclude:
+        excluded_set = set(_zen_exclude)
+        kwargs = {k: v for k, v in kwargs.items() if k not in excluded_set}
+
     if _zen_partial is True:
         return _functools.partial(obj, *args, **kwargs)
     return obj(*args, **kwargs)
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index b7d6f244..5bddae30 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -24,7 +24,7 @@ from typing import (
     overload,
 )
 
-from typing_extensions import Final, Literal
+from typing_extensions import Final, Literal, TypeGuard
 
 from hydra_zen.errors import HydraZenDeprecationWarning
 from hydra_zen.funcs import get_obj, partial, zen_processing
@@ -45,6 +45,7 @@ _TARGET_FIELD_NAME: Final[str] = "_target_"
 _RECURSIVE_FIELD_NAME: Final[str] = "_recursive_"
 _CONVERT_FIELD_NAME: Final[str] = "_convert_"
 _PARTIAL_TARGET_FIELD_NAME: Final[str] = "_zen_partial"
+_META_FIELD_NAME: Final[str] = "_zen_exclude"
 _ZEN_TARGET_FIELD_NAME: Final[str] = "_zen_target"
 _POS_ARG_FIELD_NAME: Final[str] = "_args_"
 _JUST_FIELD_NAME: Final[str] = "path"
@@ -159,6 +160,7 @@ def hydrated_dataclass(
     hydra_partial: bool = False,
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_meta: Optional[Mapping[str, Any]] = None,
     frozen: bool = False,
 ) -> Callable[[Type[_T]], Type[_T]]:
     """A decorator that uses `hydra_zen.builds` to create a dataclass with the appropriate
@@ -202,17 +204,19 @@ def hydrated_dataclass(
 
         If ``None``, the ``_convert_`` attribute is not set on the resulting dataclass.
 
+    hydra_meta: Optional[Mapping[str, Any]]
+        Specifies field-names and corresponding values that will be included in the
+        resulting dataclass, but that will *not* be used to build ``hydra_target``
+        via instantiation. These are called "meta" fields.
+
     frozen : bool, optional (default=False)
         If `True`, the resulting dataclass will create frozen (i.e. immutable) instances.
         I.e. setting/deleting an attribute of an instance will raise `FrozenInstanceError`
         at runtime.
 
-    Notes
-    -----
-    Using any of the following features will result in a config that depends explicitly on hydra-zen
-
-       - ``hydra_partial=True``
-       - Providing a class-object or function argument to target, which will automatically be wrapped by `just`.
+    See Also
+    --------
+    builds
 
     References
     ----------
@@ -292,6 +296,7 @@ def hydrated_dataclass(
             hydra_recursive=hydra_recursive,
             hydra_convert=hydra_convert,
             hydra_partial=hydra_partial,
+            hydra_meta=hydra_meta,
             builds_bases=(decorated_obj,),
             dataclass_name=decorated_obj.__name__,
             frozen=frozen,
@@ -400,6 +405,7 @@ def builds(
     hydra_partial: Literal[False] = False,
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_meta: Optional[Mapping[str, Any]] = None,
     dataclass_name: Optional[str] = None,
     builds_bases: Tuple[Any, ...] = (),
     frozen: bool = False,
@@ -417,6 +423,7 @@ def builds(
     hydra_partial: Literal[True],
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_meta: Optional[Mapping[str, Any]] = None,
     dataclass_name: Optional[str] = None,
     builds_bases: Tuple[Any, ...] = (),
     frozen: bool = False,
@@ -434,6 +441,7 @@ def builds(
     hydra_partial: bool,
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_meta: Optional[Mapping[str, Any]] = None,
     dataclass_name: Optional[str] = None,
     builds_bases: Tuple[Any, ...] = (),
     frozen: bool = False,
@@ -451,6 +459,7 @@ def builds(
     hydra_partial: bool = False,
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_meta: Optional[Mapping[str, Any]] = None,
     frozen: bool = False,
     dataclass_name: Optional[str] = None,
     builds_bases: Tuple[Any, ...] = (),
@@ -516,6 +525,11 @@ def builds(
 
         If ``None``, the ``_convert_`` attribute is not set on the resulting dataclass.
 
+    hydra_meta: Optional[Mapping[str, Any]]
+        Specifies field-names and corresponding values that will be included in the
+        resulting dataclass, but that will *not* be used to build ``hydra_target``
+        via instantiation. These are called "meta" fields.
+
     frozen : bool, optional (default=False)
         If ``True``, the resulting dataclass will create frozen (i.e. immutable) instances.
         I.e. setting/deleting an attribute of an instance will raise ``FrozenInstanceError``
@@ -542,17 +556,21 @@ def builds(
 
     Notes
     -----
-    Using any of the following features will result in a config that depends explicitly on hydra-zen.
-    (i.e. hydra-zen must be installed in order to instantiate the resulting config, including its yaml version).
+    Using any of the following features will result in a config that depends
+    explicitly on hydra-zen. (i.e. hydra-zen must be installed in order to
+    instantiate the resulting config, including its yaml version).
 
-       - ``hydra_partial=True``
+       - Specifying: ``hydra_partial=True``
+       - Specifying meta fields via ``hydra_meta=<...>``
        - Providing a class-object or function argument to ``<hydra_target>``, which will automatically be wrapped by `just`. E.g. ``builds(dict, fn=len)``
 
-    Type annotations are inferred from the target's signature and are only retained if they are compatible
-    with hydra's limited set of supported annotations; otherwise `Any` is specified.
+    Type annotations are inferred from the target's signature and are only
+    retained if they are compatible with hydra's limited set of supported
+    annotations; otherwise `Any` is specified.
 
-    `builds` provides runtime validation of user-specified named arguments against the target's signature.
-    This helps to ensure that typos in field names fail early and explicitly.
+    `builds` provides runtime validation of user-specified named arguments against
+    the target's signature. This helps to ensure that typos in field names
+    fail early and explicitly.
 
     Mutable values are automatically transformed to use a default factory [4]_.
 
@@ -605,6 +623,14 @@ def builds(
     {'a': 1, 'b': -2, 'c': -3}
     >>> issubclass(ChildConf, ParentConf)
     True
+
+    Leveraging meta-fields for portable, relative interpolation:
+
+    >>> Conf = builds(dict, a="${.s}", b="${.s}", hydra_meta=dict(s=-10))
+    >>> instantiate(Conf)
+    {'a': -10, 'b': -10}
+    >>> instantiate(Conf, s=2)
+    {'a': 2, 'b': 2}
     """
 
     if not pos_args and not kwargs_for_target:
@@ -661,26 +687,40 @@ def builds(
             + "`builds(..., hydra_partial=True)` requires that `hydra_recursive=True`"
         )
 
+    if hydra_meta is None:
+        hydra_meta = {}
+
+    if not isinstance(hydra_meta, Mapping):
+        raise TypeError(
+            f"`hydra_meta` must be a mapping (e.g. a dictionary), got: {hydra_meta}"
+        )
+
+    for _key in hydra_meta:
+        if not isinstance(_key, str):
+            raise TypeError(
+                f"`hydra_meta` must be a mapping whose keys are strings, got key: {_key}"
+            )
+
     # Check for reserved names
-    for name in kwargs_for_target:
-        if name in _HYDRA_FIELD_NAMES:
-            err_msg = f"The field-name specified via `builds(..., {name}=<...>)` is reserved by Hydra."
-            if name != _TARGET_FIELD_NAME:
+    for _name in chain(kwargs_for_target, hydra_meta):
+        if _name in _HYDRA_FIELD_NAMES:
+            err_msg = f"The field-name specified via `builds(..., {_name}=<...>)` is reserved by Hydra."
+            if _name != _TARGET_FIELD_NAME:
                 raise ValueError(
                     err_msg
-                    + f" You can set this parameter via `builds(..., hydra_{name[1:-1]}=<...>)`"
+                    + f" You can set this parameter via `builds(..., hydra_{_name[1:-1]}=<...>)`"
                 )
             else:
                 raise ValueError(err_msg)
-        if name.startswith(("hydra_", "_zen_")):
+        if _name.startswith(("hydra_", "_zen_")):
             raise ValueError(
-                f"The field-name specified via `builds(..., {name}=<...>)` is reserved by hydra-zen."
+                f"The field-name specified via `builds(..., {_name}=<...>)` is reserved by hydra-zen."
                 " You can manually create a dataclass to utilize this name in a structured config."
             )
 
     target_field: List[Union[Tuple[str, Type[Any]], Tuple[str, Type[Any], Field[Any]]]]
 
-    if hydra_partial is True:
+    if hydra_partial or hydra_meta:
         target_field = [
             (
                 _TARGET_FIELD_NAME,
@@ -692,12 +732,23 @@ def builds(
                 str,
                 _utils.field(default=_utils.get_obj_path(target), init=False),
             ),
-            (
-                _PARTIAL_TARGET_FIELD_NAME,
-                bool,
-                _utils.field(default=True, init=False),
-            ),
         ]
+        if hydra_partial:
+            target_field.append(
+                (
+                    _PARTIAL_TARGET_FIELD_NAME,
+                    bool,
+                    _utils.field(default=True, init=False),
+                ),
+            )
+        if hydra_meta:
+            target_field.append(
+                (
+                    _META_FIELD_NAME,
+                    bool,
+                    _utils.field(default=tuple(hydra_meta), init=False),
+                ),
+            )
     else:
         target_field = [
             (
@@ -816,7 +867,11 @@ def builds(
                     + f"The following unexpected keyword argument(s) was specified for {_utils.get_obj_path(target)} "
                     f"via `builds`: {', '.join(_unexpected)}"
                 )
-            if not fields_set_by_bases <= nameable_params_in_sig:
+            if not fields_set_by_bases <= nameable_params_in_sig and not (
+                fields_set_by_bases - nameable_params_in_sig
+            ) <= set(hydra_meta):
+                # field inherited by base is not present in sig
+                # AND it is not excluded via `hydra_meta`
                 _unexpected = fields_set_by_bases - nameable_params_in_sig
                 raise TypeError(
                     _utils.building_error_prefix(target)
@@ -898,6 +953,34 @@ def builds(
         for name, value in kwargs_for_target.items()
     }
 
+    if hydra_meta:
+        _meta_names = set(hydra_meta)
+
+        if _meta_names & nameable_params_in_sig:
+            raise ValueError(
+                f"`builds(..., hydra_meta=<...>)`: `hydra_meta` cannot not specify "
+                f"names that exist in the target's signature: "
+                f"{','.join(_meta_names & nameable_params_in_sig)}"
+            )
+
+        if _meta_names & set(user_specified_named_params):
+            raise ValueError(
+                f"`builds(..., hydra_meta=<...>)`: `hydra_meta` cannot not specify "
+                f"names that are common with those specified in **kwargs_for_target: "
+                f"{','.join(_meta_names & set(user_specified_named_params))}"
+            )
+
+        # We don't check for collisions between `hydra_meta` names and the
+        # names of inherited fields. Thus `hydra_meta` can effectively be used
+        # to "delete" names from a config, via inheritance.
+
+        user_specified_named_params.update(
+            {
+                name: (name, Any, sanitized_default_value(value))
+                for name, value in hydra_meta.items()
+            }
+        )
+
     if populate_full_signature is True:
         # Populate dataclass fields based on the target's signature.
         #
@@ -1014,11 +1097,11 @@ def builds(
 # with omegaconf containers.
 #
 # These are not part of the public API for now, but they may be in the future.
-def is_builds(x: Any) -> bool:
+def is_builds(x: Any) -> TypeGuard[Builds]:
     return hasattr(x, _TARGET_FIELD_NAME)
 
 
-def is_just(x: Any) -> bool:
+def is_just(x: Any) -> TypeGuard[Just]:
     if is_builds(x) and hasattr(x, _JUST_FIELD_NAME):
         attr = _get_target(x)
         if attr == _get_target(Just) or attr is get_obj:
@@ -1044,7 +1127,7 @@ def _is_old_partial_builds(x: Any) -> bool:  # pragma: no cover
     return False
 
 
-def is_partial_builds(x: Any) -> bool:
+def is_partial_builds(x: Any) -> TypeGuard[PartialBuilds]:
     if not is_builds(x) or not hasattr(x, _ZEN_TARGET_FIELD_NAME):
         return False
     attr = _get_target(x)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 3d8432e2..0c108445 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -6,3 +6,11 @@ import hypothesis.strategies as st
 valid_hydra_literals = st.sampled_from(
     [0, 1.0, True, "x", None, ["a", 1], {1: 1}, {"a": 1}]
 )
+
+
+def everything_except(*excluded_types: type):
+    return (
+        st.from_type(type)
+        .flatmap(st.from_type)
+        .filter(lambda x: not isinstance(x, excluded_types))
+    )
diff --git a/tests/test_hydra_behaviors.py b/tests/test_hydra_behaviors.py
index d9795be5..ae2fdeba 100644
--- a/tests/test_hydra_behaviors.py
+++ b/tests/test_hydra_behaviors.py
@@ -23,10 +23,11 @@ def f_three_vars(x, y, z):
     recursive=st.booleans(),
     sig=st.booleans(),
     partial=st.booleans(),
+    meta=st.none() | st.just(dict(meta=1)),
     kwargs=st.dictionaries(keys=st.sampled_from(["x", "y", "z"]), values=st.floats()),
     name=st.none() | st.sampled_from(["NameA", "NameB"]),
 )
-def test_builds_sets_hydra_params(convert, recursive, sig, partial, name, kwargs):
+def test_builds_sets_hydra_params(convert, recursive, sig, partial, name, meta, kwargs):
     if partial and not recursive:
         assume(False)
 
@@ -36,6 +37,7 @@ def test_builds_sets_hydra_params(convert, recursive, sig, partial, name, kwargs
         hydra_recursive=recursive,
         populate_full_signature=sig,
         hydra_partial=partial,
+        hydra_meta=meta,
         dataclass_name=name,
         **kwargs,
     )
diff --git a/tests/test_meta_fields.py b/tests/test_meta_fields.py
new file mode 100644
index 00000000..d7ac2be9
--- /dev/null
+++ b/tests/test_meta_fields.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2021 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+
+import string
+
+import hypothesis.strategies as st
+import pytest
+from hypothesis import given
+
+from hydra_zen import builds, hydrated_dataclass, instantiate
+from hydra_zen.structured_configs._implementations import is_partial_builds
+
+
+def f(*args, **kwargs):
+    return args, kwargs
+
+
+@given(
+    args=st.tuples(st.integers()),
+    kwargs=st.dictionaries(
+        st.text(string.ascii_letters, min_size=1, max_size=1),
+        st.integers(),
+    ),
+    hydra_meta=st.dictionaries(
+        st.text(string.ascii_letters, min_size=2, max_size=2).filter(
+            lambda x: x not in {"is", "in", "as", "or", "if"}  # reserved fields
+        ),
+        st.integers(),
+    ),
+    hydra_partial=st.booleans(),
+    pop_sig=st.booleans(),
+)
+def test_basic_hydra_meta_behavior(
+    args: tuple,
+    kwargs: dict,
+    hydra_meta: dict,
+    hydra_partial: bool,
+    pop_sig: bool,
+):
+    Conf = builds(
+        f,
+        *args,
+        **kwargs,
+        hydra_meta=hydra_meta,
+        hydra_partial=hydra_partial,
+        populate_full_signature=pop_sig
+    )
+
+    conf = Conf()
+
+    # ensure all meta-fields are present
+    for meta_name, meta_val in hydra_meta.items():
+        assert getattr(conf, meta_name) == meta_val
+
+    if not hydra_partial:
+        out_args, out_kwargs = instantiate(Conf)
+    else:
+        assert is_partial_builds(Conf)
+        out_args, out_kwargs = instantiate(Conf)()
+
+    assert out_args == args
+    assert out_kwargs == kwargs
+    assert set(out_kwargs).isdisjoint(hydra_meta)
+
+
+def test_hydra_meta_via_hydrated_dataclass():
+    @hydrated_dataclass(dict, hydra_meta=dict(a=1))
+    class Conf:
+        b: int = 2
+
+    conf = Conf()
+    assert conf.a == 1
+    assert conf.b == 2
+    assert instantiate(conf) == dict(b=2)
+
+
+def test_mutable_meta_value_gets_wrapped():
+    Conf = builds(int, hydra_meta=dict(a=[1, 2]))
+    conf1 = Conf()
+    assert conf1.a == [1, 2]
+    conf1.a.append(3)
+
+    conf2 = Conf()
+    assert conf2.a == [1, 2]
+
+
+def f2(*, x):
+    return x
+
+
+def test_deletion_by_inheritance():
+    Conf = builds(dict, not_compat_with_f=1, x=3)
+
+    with pytest.raises(TypeError):
+        # presence of `not_compat_with_f` should raise
+        builds(f2, builds_bases=(Conf,))
+
+    out = instantiate(
+        builds(
+            f2,
+            # `not_compat_with_f` should no longer be flagged
+            # by our signature verification since it is now
+            # a meta field.
+            # We have effectively "deleted" `not_compat_with_f`
+            # from this config.
+            hydra_meta=dict(not_compat_with_f=None),
+            builds_bases=(Conf,),
+        )
+    )
+    assert out == 3
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 766a313b..7b06b41e 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: MIT
 import inspect
 from collections import Counter
+from collections.abc import Mapping
 from dataclasses import dataclass, is_dataclass
 from itertools import zip_longest
 
@@ -16,6 +17,7 @@ from hydra_zen.structured_configs._implementations import (
     _HYDRA_FIELD_NAMES,
     _ZEN_TARGET_FIELD_NAME,
 )
+from tests import everything_except
 
 
 def test_builds_no_args_raises():
@@ -60,6 +62,17 @@ def test_builds_hydra_partial_raises_if_recursion_disabled():
         builds(dict, hydra_partial=True, hydra_recursive=False)
 
 
+@given(everything_except(Mapping, type(None)))
+def test_builds_hydra_meta_not_mapping_raises(not_a_mapping):
+    with pytest.raises(TypeError):
+        builds(int, hydra_meta=not_a_mapping)
+
+
+def test_builds_hydra_meta_with_non_string_keys_raises():
+    with pytest.raises(TypeError):
+        builds(int, hydra_meta={1: None})
+
+
 def f_starx(*x):
     pass
 
@@ -129,6 +142,7 @@ def test_builds_raises_when_user_specified_args_violate_sig(
             populate_full_signature=full_sig,
             builds_bases=(kwarg_base,),
         )
+    del kwarg_base
 
     # test when *args are inherited
     args_base = builds(passthrough, *args, hydra_partial=partial)
@@ -140,16 +154,18 @@ def test_builds_raises_when_user_specified_args_violate_sig(
             populate_full_signature=full_sig,
             builds_bases=(args_base,),
         )
+    del args_base
 
     # test when *args and **kwargs are inherited
-    args_base = builds(passthrough, *args, **kwargs, hydra_partial=partial)
+    args_kwargs_base = builds(passthrough, *args, **kwargs, hydra_partial=partial)
     with pytest.raises(TypeError):
         builds(
             func,
             hydra_partial=partial,
             populate_full_signature=full_sig,
-            builds_bases=(args_base,),
+            builds_bases=(args_kwargs_base,),
         )
+    del args_kwargs_base
 
 
 @dataclass
@@ -328,3 +344,61 @@ def test_reserved_names_are_reserved(field: str):
     kwargs = {field: True}
     with pytest.raises(ValueError):
         builds(dict, **kwargs)
+
+
+@pytest.mark.parametrize(
+    "field",
+    list(_HYDRA_FIELD_NAMES)
+    + [_ZEN_TARGET_FIELD_NAME, "_zen_some_new_feature", "hydra_some_new_feature"],
+)
+def test_reserved_names_are_reserved_by_hydra_meta(field: str):
+    kwargs = {field: True}
+    with pytest.raises(ValueError):
+        builds(dict, hydra_meta=kwargs)
+
+
+def f_meta_sig(x, *args, y, **kwargs):
+    pass
+
+
+@given(
+    meta_fields=st.dictionaries(
+        st.sampled_from(["x", "y", "args", "kwargs", "z"]), st.integers()
+    ),
+    pop_sig=st.booleans(),
+    partial=st.booleans(),
+)
+def test_meta_fields_colliding_with_sig_raises(
+    meta_fields, pop_sig: bool, partial: bool
+):
+    if {"x", "y"} & set(meta_fields):
+        with pytest.raises(ValueError):
+            builds(
+                f_meta_sig,
+                hydra_meta=meta_fields,
+                populate_full_signature=pop_sig,
+                hydra_partial=partial,
+            )
+    else:
+        builds(
+            f_meta_sig,
+            hydra_meta=meta_fields,
+            populate_full_signature=pop_sig,
+            hydra_partial=partial,
+        )
+
+
+@given(
+    meta_fields=st.dictionaries(
+        st.sampled_from(["x", "y", "args", "kwargs", "z"]), st.integers()
+    ),
+    partial=st.booleans(),
+)
+def test_meta_fields_colliding_with_user_provided_kwargs_raises(
+    meta_fields, partial: bool
+):
+    if {"x", "y"} & set(meta_fields):
+        with pytest.raises(ValueError):
+            builds(dict, x=1, y=2, hydra_meta=meta_fields, hydra_partial=partial)
+    else:
+        builds(dict, x=1, y=2, hydra_meta=meta_fields, hydra_partial=partial)