# SPDX-License-Identifier: MIT
import functools
import inspect
import sys
import warnings
from collections import Counter, defaultdict, deque
from collections.abc import Collection
from dataclasses import ( # use this for runtime checks
MISSING,
Field as _Field,
InitVar,
dataclass,
field,
fields,
is_dataclass,
make_dataclass,
from datetime import timedelta
from enum import Enum
from functools import partial
from itertools import chain
from pathlib import Path, PosixPath, WindowsPath
from typing import (
Any,
Callable,
Dict,
FrozenSet,
Generic,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Type,
TypeVar,
Union,
cast,
get_args,
get_origin,
get_type_hints,
overload,
from omegaconf import DictConfig, ListConfig
from typing_extensions import (
Annotated,
Concatenate,
Final,
Literal,
ParamSpec,
ParamSpecArgs,
ParamSpecKwargs,
Protocol,
Self,
TypeAlias,
Unpack,
_AnnotatedAlias,
dataclass_transform,
from hydra_zen._compatibility import (
HYDRA_SUPPORTED_PRIMITIVE_TYPES,
HYDRA_SUPPORTED_PRIMITIVES,
OMEGACONF_VERSION,
ZEN_SUPPORTED_PRIMITIVES,
Version,
from hydra_zen.errors import (
HydraZenDeprecationWarning,
HydraZenUnsupportedPrimitiveError,
HydraZenValidationError,
from hydra_zen.funcs import as_default_dict, get_obj
from hydra_zen.structured_configs import _utils
from hydra_zen.structured_configs._type_guards import safe_getattr
from hydra_zen.typing import (
Builds,
DataclassOptions,
PartialBuilds,
SupportedPrimitive,
ZenConvert,
ZenWrappers,
from hydra_zen.typing._implementations import (
AllConvert,
AnyBuilds,
Builds,
BuildsWithSig,
DataClass,
DataClass_,
DataclassOptions,
DefaultsList,
Field,
HasTarget,
HasTargetInst,
HydraSupportedType,
InstOrType,
Just as JustT,
Partial,
ZenConvert,
_HydraPrimitive,
_SupportedViaBuilds,
from ._globals import (
CONVERT_FIELD_NAME,
DEFAULTS_LIST_FIELD_NAME,
HYDRA_FIELD_NAMES,
JUST_FIELD_NAME,
META_FIELD_NAME,
PARTIAL_FIELD_NAME,
POS_ARG_FIELD_NAME,
RECURSIVE_FIELD_NAME,
TARGET_FIELD_NAME,
ZEN_PARTIAL_FIELD_NAME,
ZEN_PROCESSING_LOCATION,
ZEN_TARGET_FIELD_NAME,
ZEN_WRAPPERS_FIELD_NAME,
from ._type_guards import (
is_builds,
is_generic_type,
is_just,
is_old_partial_builds,
safe_getattr,
uses_zen_processing,
from ._utils import merge_settings
T = TypeVar("T")
_T = TypeVar("_T")
P = ParamSpec("P")
R = TypeVar("R")
TD = TypeVar("TD", bound=DataClass_)
TC = TypeVar("TC", bound=Callable[..., Any])
TP = TypeVar("TP", bound=_HydraPrimitive)
TB = TypeVar("TB", bound=Union[_SupportedViaBuilds, FrozenSet[Any]])
Importable = TypeVar("Importable", bound=Callable[..., Any])
Field_Entry: TypeAlias = Tuple[str, type, Field[Any]]
_JUST_CONVERT_SETTINGS = AllConvert(dataclass=True, flat_target=False)
# default zen_convert settings for `builds` and `hydrated_dataclass`
_BUILDS_CONVERT_SETTINGS = AllConvert(dataclass=True, flat_target=True)
# stores type -> value-conversion-fn
# for types with specialized support from hydra-zen
class _ConversionFn(Protocol):
def __call__(self, __x: Any, CBuildsFn: "Type[BuildsFn[Any]]") -> Any: ...
ZEN_VALUE_CONVERSION: Dict[type, _ConversionFn] = {}
# signature param-types
_POSITIONAL_ONLY: Final = inspect.Parameter.POSITIONAL_ONLY
_POSITIONAL_OR_KEYWORD: Final = inspect.Parameter.POSITIONAL_OR_KEYWORD
_VAR_POSITIONAL: Final = inspect.Parameter.VAR_POSITIONAL
_KEYWORD_ONLY: Final = inspect.Parameter.KEYWORD_ONLY
_VAR_KEYWORD: Final = inspect.Parameter.VAR_KEYWORD
NoneType = type(None)
_supported_types = HYDRA_SUPPORTED_PRIMITIVE_TYPES | {
list,
dict,
tuple,
List,
Tuple,
Dict,
_builtin_function_or_method_type = type(len)
# fmt: off
_lru_cache_type = type(functools.lru_cache(maxsize=128)(lambda: None)) # pragma: no branch
# fmt: on
_BUILTIN_TYPES: Final = (_builtin_function_or_method_type, _lru_cache_type)
del _lru_cache_type
del _builtin_function_or_method_type
def _retain_type_info(type_: type, value: Any, hydra_recursive: Optional[bool]):
# OmegaConf's type-checking occurs before instantiation occurs.
# This means that, e.g., passing `Builds[int]` to a field `x: int`
# will fail Hydra's type-checking upon instantiation, even though
# the recursive instantiation will appropriately produce `int` for
# that field. This will not be addressed by hydra/omegaconf:
# https://github.com/facebookresearch/hydra/issues/1759
# Thus we will auto-broaden the annotation when we see that a field
# is set with a structured config as a default value - assuming that
# the field isn't annotated with a structured config type.
# Each condition is included separately to ensure that our tests
# cover all scenarios
if hydra_recursive is False:
return True
elif not is_builds(value):
if _utils.is_interpolated_string(value):
# an interpolated field may resolve to a structured conf, which may
# instantiate to a value of the specified type
return False
return True
elif is_builds(type_):
return True
return False
[docs]
@dataclass_transform()
def hydrated_dataclass(
target: Callable[..., Any],
*pos_args: SupportedPrimitive,
zen_partial: Optional[bool] = None,
zen_wrappers: ZenWrappers[Callable[..., Any]] = tuple(),
zen_meta: Optional[Mapping[str, Any]] = None,
populate_full_signature: bool = False,
hydra_recursive: Optional[bool] = None,
hydra_convert: Optional[Literal["none", "partial", "all", "object"]] = None,
zen_convert: Optional[ZenConvert] = None,
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = True,
frozen: bool = False,
match_args: bool = True,
kw_only: bool = False,
slots: bool = False,
weakref_slot: bool = False,
) -> Callable[[Type[_T]], Type[_T]]:
"""A decorator that uses `builds` to create a dataclass with the appropriate
Hydra-specific fields for specifying a targeted config [1]_.
This provides similar functionality to `builds`, but enables a user to define
a config explicitly using the :func:`dataclasses.dataclass` syntax, which can
enable enhanced static analysis of the resulting config.
Parameters
----------
hydra_target : T (Callable)
The target-object to be configured. This is a required, positional-only argument.
*pos_args : SupportedPrimitive
Positional arguments passed as ``hydra_target(*pos_args, ...)`` upon instantiation.
Arguments specified positionally are not included in the dataclass' signature
and are stored as a tuple bound to in the ``_args_`` field.
zen_partial : Optional[bool]
If ``True``, then the resulting config will instantiate as
``functools.partial(hydra_target, *pos_args, **kwargs_for_target)`` rather than
``hydra_target(*pos_args, **kwargs_for_target)``. Thus this enables the
partial-configuration of objects.
Specifying ``zen_partial=True`` and ``populate_full_signature=True`` together
will populate the config's signature only with parameters that: are explicitly
specified by the user, or that have default values specified in the target's
signature. I.e. it is presumed that un-specified parameters that have no
default values are to be excluded from the config.
zen_wrappers : None | Callable | Builds | InterpStr | Sequence[None | Callable | Builds | InterpStr]
One or more wrappers, which will wrap ``hydra_target`` prior to instantiation.
E.g. specifying the wrappers ``[f1, f2, f3]`` will instantiate as::
f3(f2(f1(hydra_target)))(*args, **kwargs)
Wrappers can also be specified as interpolated strings [2]_ or targeted
configs.
zen_meta : Optional[Mapping[str, SupportedPrimitive]]
Specifies field-names and corresponding values that will be included in the
resulting config, but that will *not* be used to builds ``<hydra_target>``
via instantiation. These are called "meta" fields.
populate_full_signature : bool, optional (default=False)
If ``True``, then the resulting config's signature and fields will be populated
according to the signature of ``hydra_target``; values also specified in
``**kwargs_for_target`` take precedent.
This option is not available for objects with inaccessible signatures, such as
NumPy's various ufuncs.
zen_convert : Optional[ZenConvert]
A dictionary that modifies hydra-zen's value and type conversion behavior.
Consists of the following optional key-value pairs (:ref:`zen-convert`):
- `dataclass` : `bool` (default=True):
If `True` any dataclass type/instance without a
`_target_` field is automatically converted to a targeted config
that will instantiate to that type/instance. Otherwise the dataclass
type/instance will be passed through as-is.
hydra_recursive : Optional[bool], optional (default=True)
If ``True``, then Hydra will recursively instantiate all other
hydra-config objects nested within this config [3]_.
If ``None``, the ``_recursive_`` attribute is not set on the resulting config.
- ``"none"``: No conversion occurs; omegaconf containers are passed through (Default)
- ``"partial"``: ``DictConfig`` and ``ListConfig`` objects converted to ``dict`` and
``list``, respectively. Structured configs and their fields are passed without conversion.
- ``"all"``: All passed objects are converted to dicts, lists, and primitives, without a trace of OmegaConf containers.
If ``None``, the ``_convert_`` attribute is not set on the resulting config.
hydra_convert : Optional[Literal["none", "partial", "all", "object"]], optional (default="none")
Determines how Hydra treats the non-primitive, omegaconf-specific objects
during instantiateion [3]_.
- ``"none"``: No conversion occurs; omegaconf containers are passed through (Default)
- ``"partial"``: ``DictConfig`` and ``ListConfig`` objects converted to ``dict`` and
``list``, respectively. Structured configs and their fields are passed without conversion.
- ``"all"``: All passed objects are converted to dicts, lists, and primitives, without
a trace of OmegaConf containers.
- ``"object"``: Passed objects are converted to dict and list. Structured Configs are converted to instances of the backing dataclass / attr class.
If ``None``, the ``_convert_`` attribute is not set on the resulting config.
init : bool, optional (default=True)
If true (the default), a __init__() method will be generated. If the class
already defines __init__(), this parameter is ignored.
repr : bool, optional (default=True)
If true (the default), a `__repr__()` method will be generated. The generated
repr string will have the class name and the name and repr of each field, in
the order they are defined in the class. Fields that are marked as being
excluded from the repr are not included. For example:
`InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)`.
eq : bool, optional (default=True)
If true (the default), an __eq__() method will be generated. This method
compares the class as if it were a tuple of its fields, in order. Both
instances in the comparison must be of the identical type.
order : bool, optional (default=False)
If true (the default is `False`), `__lt__()`, `__le__()`, `__gt__()`, and
`__ge__()` methods will be generated. These compare the class as if it were a
tuple of its fields, in order. Both instances in the comparison must be of the
identical type. If order is true and eq is false, a ValueError is raised.
If the class already defines any of `__lt__()`, `__le__()`, `__gt__()`, or
`__ge__()`, then `TypeError` is raised.
unsafe_hash : bool, optional (default=False)
If `False` (the default), a `__hash__()` method is generated according to how
`eq` and `frozen` are set.
If `eq` and `frozen` are both true, by default `dataclass()` will generate a
`__hash__()` method for you. If `eq` is true and `frozen` is false, `__hash__()
` will be set to `None`, marking it unhashable. If `eq` is false, `__hash__()`
will be left untouched meaning the `__hash__()` method of the superclass will
be used (if the superclass is object, this means it will fall back to id-based
hashing).
frozen : bool, optional (default=False)
If true (the default is `False`), assigning to fields will generate an
exception. This emulates read-only frozen instances.
match_args : bool, optional (default=True)
(*New in version 3.10*) If true (the default is `True`), the `__match_args__`
tuple will be created from the list of parameters to the generated `__init__()`
method (even if `__init__()` is not generated, see above). If false, or if
`__match_args__` is already defined in the class, then `__match_args__` will
not be generated.
kw_only : bool, optional (default=False)
(*New in version 3.10*) If true (the default value is `False`), then all fields
will be marked as keyword-only.
slots : bool, optional (default=False)
(*New in version 3.10*) If true (the default is `False`), `__slots__` attribute
will be generated and new class will be returned instead of the original one.
If `__slots__` is already defined in the class, then `TypeError` is raised.
weakref_slot : bool, optional (default=False)
(*New in version 3.11*) If true (the default is `False`), add a slot named
“__weakref__”, which is required to make an instance weakref-able. It is an
error to specify `weakref_slot=True` without also specifying `slots=True`.
See Also
--------
builds : Create a targeted structured config designed to "build" a particular object.
Raises
------
hydra_zen.errors.HydraZenUnsupportedPrimitiveError
The provided configured value cannot be serialized by Hydra, nor does hydra-zen
provide specialized support for it. See :ref:`valid-types` for more details.
Notes
-----
Unlike `builds`, `hydrated_dataclass` enables config fields to be set explicitly
with custom type annotations. Additionally, the resulting config' attributes
can be analyzed by static tooling, which can help to warn about errors prior
to running one's code.
For details of the annotation `SupportedPrimitive`, see :ref:`valid-types`.
References
----------
.. [1] https://hydra.cc/docs/tutorials/structured_config/intro/
.. [2] https://omegaconf.readthedocs.io/en/2.1_branch/usage.html#variable-interpolation
.. [3] https://hydra.cc/docs/advanced/instantiate_objects/overview/#recursive-instantiation
.. [4] https://hydra.cc/docs/advanced/instantiate_objects/overview/#parameter-conversion-strategies
Examples
--------
**Basic usage**
>>> from hydra_zen import hydrated_dataclass, instantiate
Here, we specify a config that is designed to "build" a dictionary
upon instantiation
>>> @hydrated_dataclass(target=dict, frozen=True)
... class DictConf:
... x: int = 2
... y: str = 'hello'
>>> instantiate(DictConf(x=10)) # override default `x`
{'x': 10, 'y': 'hello'}
>>> d = DictConf()
>>> # Static type checker marks the following as
>>> # an error because `d` is frozen.
>>> d.x = 3 # type: ignore
FrozenInstanceError: cannot assign to field 'x'
For more detailed examples, refer to `builds`.
def wrapper(decorated_obj: Any) -> Any:
if not isinstance(decorated_obj, type):
raise NotImplementedError(
"Class instances are not supported by `hydrated_dataclass`."
# TODO: We should mutate `decorated_obj` directly like @dataclass does.
# Presently, we create an intermediate dataclass that we inherit
# from, which gets the job done for the most part but there are
# practical differences. E.g. you cannot delete an attribute that
# was declared in the definition of `decorated_obj`.
dc_options = _utils.parse_dataclass_options(
"init": init,
"repr": repr,
"eq": eq,
"order": order,
"unsafe_hash": unsafe_hash,
"frozen": frozen,
"match_args": match_args,
"kw_only": kw_only,
"slots": slots,
"weakref_slot": weakref_slot,
include_module=False,
decorated_obj = dataclass(**dc_options)(decorated_obj) # type: ignore
if populate_full_signature:
# we need to ensure that the fields specified via the class definition
# take precedence over the fields that will be auto-populated by builds
kwargs = {
f.name: f.default if f.default is not MISSING else f.default_factory() # type: ignore
for f in fields(decorated_obj)
if not (f.default is MISSING and f.default_factory is MISSING)
and f.name not in HYDRA_FIELD_NAMES
and not f.name.startswith("_zen_")
else:
kwargs: Dict[str, Any] = {}
out = DefaultBuilds.builds(
target,
*pos_args,
**kwargs,
populate_full_signature=populate_full_signature,
hydra_recursive=hydra_recursive,
hydra_convert=hydra_convert,
zen_wrappers=zen_wrappers,
zen_partial=zen_partial,
zen_meta=zen_meta,
builds_bases=(decorated_obj,),
zen_dataclass={
"cls_name": decorated_obj.__name__,
"module": decorated_obj.__module__,
"init": init,
"repr": repr,
"eq": eq,
"order": order,
"unsafe_hash": unsafe_hash,
"frozen": frozen,
"match_args": match_args,
"kw_only": kw_only,
"slots": slots,
"weakref_slot": weakref_slot,
zen_convert=zen_convert,
return out
return wrapper