Source code for creyone_model.utils.config
from dataclasses import dataclass, fields
from typing import Any
[docs]
def type_subclass(cls: type, classinfo) -> bool:
"""Call issubclass safely, returning False if cls is not a type (e.g. MISSING)."""
try:
return issubclass(cls, classinfo)
except TypeError:
return False
[docs]
@dataclass
class BaseCfg:
"""Base class for hierarchical configuration dataclasses.
Subclasses should be decorated with ``@dataclass`` and may declare fields
of primitive types or other ``BaseCfg`` subclasses. Fields whose
``default_factory`` is a ``BaseCfg`` subclass are built recursively by
``instance()``.
"""
[docs]
@classmethod
def instance(cls, **kwargs) -> tuple[Any, dict[str, Any]]:
"""Build an instance from kwargs and return any unconsumed kwargs.
Iterates over all dataclass fields, popping matching keys from kwargs.
For fields whose ``default_factory`` is a ``BaseCfg`` subclass, the
method recurses, passing both the field-specific kwargs and all
remaining kwargs so that shared parameters propagate down. Each nested
call returns its own unconsumed kwargs, which continue up the chain.
Args:
**kwargs: Keyword arguments keyed by field name. Values for nested
``BaseCfg`` fields should be provided as a ``dict`` of
sub-kwargs for that config.
Returns:
A ``(instance, remaining_kwargs)`` tuple where ``remaining_kwargs``
holds any kwargs not consumed by this class or its nested configs.
"""
kw: dict[str, Any] = {}
for f in fields(cls):
if f.name in kwargs: kw[f.name] = kwargs.pop(f.name)
if not type_subclass(f.default_factory, BaseCfg): continue
# Field-specific kwargs take priority over shared remaining kwargs.
# Build a new dict so the caller's original dict is never mutated.
temp = {**kwargs, **kw.get(f.name, {})}
kw[f.name], kwargs = f.default_factory.instance(**temp)
return cls(**kw), kwargs