Skip to content

dict

DictConvertible

Bases: ABC

Mixin class enabling conversion of an object to/from a Python dict.

Subclasses should override to_dict and from_dict to implement the conversion.

Source code in fancy_dataclass/dict.py
class DictConvertible(ABC):
    """Mixin class enabling conversion of an object to/from a Python dict.

    Subclasses should override `to_dict` and `from_dict` to implement the conversion."""

    @abstractmethod
    def to_dict(self, **kwargs: Any) -> AnyDict:
        """Converts an object to a dict.

        Args:
            kwargs: Keyword arguments

        Returns:
            A Python dict"""

    @classmethod
    @abstractmethod
    def from_dict(cls, d: AnyDict, **kwargs: Any) -> Self:
        """Constructs an object from a dictionary of (attribute, value) pairs.

        Args:
            d: Dict to convert into an object
            kwargs: Keyword arguments

        Returns:
            Converted object of this class"""

from_dict(d, **kwargs) abstractmethod classmethod

Constructs an object from a dictionary of (attribute, value) pairs.

Parameters:

Name Type Description Default
d AnyDict

Dict to convert into an object

required
kwargs Any

Keyword arguments

{}

Returns:

Type Description
Self

Converted object of this class

Source code in fancy_dataclass/dict.py
@classmethod
@abstractmethod
def from_dict(cls, d: AnyDict, **kwargs: Any) -> Self:
    """Constructs an object from a dictionary of (attribute, value) pairs.

    Args:
        d: Dict to convert into an object
        kwargs: Keyword arguments

    Returns:
        Converted object of this class"""

to_dict(**kwargs) abstractmethod

Converts an object to a dict.

Parameters:

Name Type Description Default
kwargs Any

Keyword arguments

{}

Returns:

Type Description
AnyDict

A Python dict

Source code in fancy_dataclass/dict.py
@abstractmethod
def to_dict(self, **kwargs: Any) -> AnyDict:
    """Converts an object to a dict.

    Args:
        kwargs: Keyword arguments

    Returns:
        A Python dict"""

DictDataclass

Bases: DataclassMixin

Mixin class for dataclasses that can be converted to and from a Python dict.

A subclass may configure settings by using DictDataclassSettings fields as keyword arguments when inheriting from DictDataclass.

Per-field settings can be passed into the metadata argument of each dataclasses.field. See DictDataclassFieldSettings for the full list of settings.

Source code in fancy_dataclass/dict.py
class DictDataclass(DataclassMixin):
    """Mixin class for dataclasses that can be converted to and from a Python dict.

    A subclass may configure settings by using [`DictDataclassSettings`][fancy_dataclass.dict.DictDataclassSettings] fields as keyword arguments when inheriting from `DictDataclass`.

    Per-field settings can be passed into the `metadata` argument of each `dataclasses.field`. See [`DictDataclassFieldSettings`][fancy_dataclass.dict.DictDataclassFieldSettings] for the full list of settings."""

    __settings_type__ = DictDataclassSettings
    __settings__ = DictDataclassSettings()
    __field_settings_type__ = DictDataclassFieldSettings

    @classmethod
    def __post_dataclass_wrap__(cls) -> None:
        super().__post_dataclass_wrap__()
        store_type = cls.__settings__.store_type or cls.__settings__.qualified_type
        if store_type:
            for fld in dataclasses.fields(cls):  # type: ignore[arg-type]
                if fld.name == 'type':
                    raise TypeError(f"'type' is a reserved dict field for {cls.__name__}, cannot be used as dataclass field")

    def _dict_init(self) -> AnyDict:
        """Gets the basic skeleton for a dict generated by this type.
        If `store_type` or `qualified_type` is set to `True`, will include a `type` field to store the type."""
        if self.__settings__.store_type:
            return {'type' : obj_class_name(self)}
        if self.__settings__.qualified_type:
            return {'type' : fully_qualified_class_name(self.__class__)}
        return {}

    @classmethod
    def _to_dict_value_basic(cls, val: Any) -> Any:
        """Converts a value with a basic type to a form appropriate for dict values.

        By default this will return the original value. Subclasses may override the behavior, e.g. to perform custom type coercion."""
        return val

    @classmethod
    def _to_dict_value(cls, val: Any, full: bool) -> Any:
        """Converts an arbitrary value to a form appropriate for dict values.

        This will recursively process values within containers (lists, dicts, etc.)."""
        if isinstance(val, DictDataclass):
            return val.to_dict(full=full)
        elif isinstance(val, list):
            return [cls._to_dict_value(elt, full) for elt in val]
        elif isinstance(val, tuple):
            return tuple(cls._to_dict_value(elt, full) for elt in val)
        elif isinstance(val, dict):
            return {k : cls._to_dict_value(v, full) for (k, v) in val.items()}
        return cls._to_dict_value_basic(val)

    def _to_dict(self, full: bool) -> AnyDict:
        if self.__settings__.flattened:
            cls = type(self)
            flat_obj = _flatten_dataclass(cls)[1].forward(self)
            return flat_obj._to_dict(full)  # type: ignore
        d = self._dict_init()
        class_suppress_defaults = self.__settings__.suppress_defaults
        for (name, fld) in self.__dataclass_fields__.items():  # type: ignore[attr-defined]
            is_class_var = get_origin(fld.type) is ClassVar
            settings = self._field_settings(fld).adapt_to(DictDataclassFieldSettings)
            # suppress field by default if it is a ClassVar or init=False
            if (is_class_var or (not fld.init)) if (settings.suppress is None) else settings.suppress:
                continue
            val = getattr(self, name)
            # suppress default (by default) if full=False and class-configured suppress_defaults=True
            if (not full) and (class_suppress_defaults if (settings.suppress_default is None) else settings.suppress_default):
                # suppress values that match the default
                try:
                    if val == fld.default:
                        continue
                    if (fld.default_factory != dataclasses.MISSING) and (val == fld.default_factory()):
                        continue
                except ValueError:  # some types may fail to compare
                    pass
            safe_dict_insert(d, name, self._to_dict_value(val, full))
        return d

    def to_dict(self, **kwargs: Any) -> AnyDict:
        """Converts the object to a Python dict which, by default, suppresses values matching their dataclass defaults.

        Args:
            kwargs: Keyword arguments <ul><li>`full`: if `True` (or if the class has `suppress_defaults=False`), does not suppress default values</li></ul>

        Returns:
            A dict whose keys match the dataclass's fields"""
        full = kwargs.get('full', False)
        return self._to_dict(full)

    @staticmethod
    def _from_dict_value_convertible(tp: Type['DictDataclass'], val: Any, strict: bool) -> Any:
        if isinstance(val, tp):  # already converted from a dict
            return val
        # otherwise, convert from a dict
        return tp.from_dict(val, strict=strict)

    @classmethod
    def _from_dict_value_basic(cls, tp: type, val: Any) -> Any:
        """Given a basic type and a value, attempts to convert the value to the given type.

        By default this will return the original value. Subclasses may override the behavior, e.g. to perform custom validation or type coercion."""
        if cls.__settings__.validate and (not isinstance(val, tp)):  # validate type
            raise TypeConversionError(tp, val)
        # NOTE: alternatively, we could coerce to the type
        # if val is None:  # do not coerce None
        #     raise TypeConversionError(tp, val)
        # try:
        #     return tp(val)  # type: ignore[call-arg]
        # except TypeError as e:
        #     raise TypeConversionError(tp, val) from e
        return val

    @classmethod
    def _from_dict_value(cls, tp: type, val: Any, strict: bool = False) -> Any:
        """Given a type and a value, attempts to convert the value to the given type."""
        def err() -> TypeConversionError:
            return TypeConversionError(tp, val)
        convert_val = partial(cls._from_dict_value, strict=strict)
        if tp is type(None):
            if val is None:
                return None
            raise err()
        if tp in [Any, 'typing.Any']:  # assume basic data type
            return val
        ttp = type(tp)
        if ttp is _AnnotatedAlias:  # Annotated: just ignore the annotation
            return convert_val(get_args(tp)[0], val)
        if issubclass_safe(tp, list):
            # class may inherit from List[T], so get the parent class
            assert hasattr(tp, '__orig_bases__')
            for base in tp.__orig_bases__:
                origin_type = get_origin(base)
                if origin_type and issubclass_safe(origin_type, list):
                    tp = base
                    break
        origin_type = get_origin(tp)
        if origin_type is None:  # basic class or type
            if ttp == TypeVar:  # type: ignore[comparison-overlap]
                # can't refer to instantiated type, so we assume a basic data type
                # this limitation means we can only use TypeVar for basic types
                return val
            if hasattr(tp, 'from_dict'):  # handle nested fields which are themselves convertible from a dict
                return cls._from_dict_value_convertible(tp, val, strict)
            if issubclass(tp, tuple):
                return tp(*val)
            if issubclass(tp, dict):
                if ttp is _TypedDictMeta:  # validate TypedDict fields
                    anns = tp.__annotations__
                    if cls.__settings__.validate and ((not isinstance(val, dict)) or (set(anns) != set(val))):
                        raise err()
                    return {key: convert_val(valtype, val[key]) for (key, valtype) in anns.items()}
                return tp(val)
            else:  # basic data type
                return cls._from_dict_value_basic(tp, val)
        else:  # compound data type
            args = get_args(tp)
            if origin_type == list:
                subtype = args[0]
                return [convert_val(subtype, elt) for elt in val]
            elif origin_type == dict:
                (keytype, valtype) = args
                return {convert_val(keytype, k) : convert_val(valtype, v) for (k, v) in val.items()}
            elif origin_type == tuple:
                subtypes = args
                if subtypes[-1] == Ellipsis:  # treat it like a list
                    subtype = subtypes[0]
                    return tuple(convert_val(subtype, elt) for elt in val)
                return tuple(convert_val(subtype, elt) for (subtype, elt) in zip(args, val))
            elif origin_type == Union:
                for subtype in args:
                    try:
                        # NB: will resolve to the first valid type in the Union
                        return convert_val(subtype, val)
                    except Exception:
                        continue
            elif origin_type == Literal:
                if any((val == arg) for arg in args):
                    # one of the Literal options is matched
                    return val
            elif hasattr(origin_type, 'from_dict'):
                return cls._from_dict_value_convertible(origin_type, val, strict)
            elif issubclass_safe(origin_type, Iterable):  # arbitrary iterable
                subtype = args[0]
                return type(val)(convert_val(subtype, elt) for elt in val)
        raise err()

    @classmethod
    def _get_missing_value(cls, fld: Field) -> Any:  # type: ignore[type-arg]
        raise ValueError(f'{fld.name!r} field is required')

    @classmethod
    def _dataclass_args_from_dict(cls, d: AnyDict, strict: bool = False) -> AnyDict:
        """Given a dict of arguments, performs type conversion and/or validity checking, then returns a new dict that can be passed to the class's constructor."""
        check_dataclass(cls)
        kwargs = {}
        bases = cls.mro()
        fields = dataclasses.fields(cls)  # type: ignore[arg-type]
        if strict:  # check there are no extraneous fields
            field_names = {field.name for field in fields}
            for key in d:
                if (key not in field_names):
                    raise ValueError(f'{key!r} is not a valid field for {cls.__name__}')
        for fld in fields:
            if not fld.init:  # suppress fields where init=False
                continue
            if fld.name in d:
                # field may be defined in the dataclass itself or one of its ancestor dataclasses
                for base in bases:
                    try:
                        field_type = base.__annotations__[fld.name]
                        kwargs[fld.name] = cls._from_dict_value(field_type, d[fld.name], strict=strict)
                        break
                    except (AttributeError, KeyError):
                        pass
                else:
                    raise ValueError(f'could not locate field {fld.name!r}')
            elif fld.default == dataclasses.MISSING:
                if fld.default_factory == dataclasses.MISSING:
                    val = cls._get_missing_value(fld)
                else:
                    val = fld.default_factory()
                    # raise ValueError(f'{fld.name!r} field is required')
                kwargs[fld.name] = val
        return kwargs

    @classmethod
    def from_dict(cls, d: AnyDict, **kwargs: Any) -> Self:
        """Constructs an object from a dictionary of fields.

        This may also perform some basic type/validity checking.

        Args:
            d: Dict to convert into an object
            kwargs: Keyword arguments <ul><li>`strict`: if `True`, raise an error if extraneous dict fields are present</li></ul>

        Returns:
            Converted object of this class"""
        # first establish the type, which may be present in the 'type' field of the dict
        typename = d.get('type')
        if typename is None:  # type field unspecified, so use the calling class
            tp = cls
        else:
            cls_name = fully_qualified_class_name(cls) if ('.' in typename) else cls.__name__
            if cls_name == typename:  # type name already matches this class
                tp = cls
            else:
                # tp must be a subclass of cls
                # the name must be in scope to be found, allowing two alternatives for retrieval:
                # option 1: all subclasses of this DictDataclass are defined in the same module as the base class
                # option 2: the name is fully qualified, so the name can be loaded into scope
                # call from_dict on the subclass in case it has its own custom implementation
                # (remove the type name before passing to the constructor)
                d2 = {key: val for (key, val) in d.items() if (key != 'type')}
                return cls._get_subclass_with_name(typename).from_dict(d2, **kwargs)
        conv = None
        if cls.__settings__.flattened:
            # produce equivalent subfield-flattened type
            settings = copy(tp.__settings__)
            settings.flattened = True
            conv = _flatten_dataclass(tp, cls.__bases__)[1]
            tp = conv.to_type  # type: ignore[assignment]
            tp.__settings__ = settings
        strict = kwargs.get('strict', False)
        result: Self = tp(**tp._dataclass_args_from_dict(d, strict=strict))
        return conv.backward(result) if cls.__settings__.flattened else result  # type: ignore

from_dict(d, **kwargs) classmethod

Constructs an object from a dictionary of fields.

This may also perform some basic type/validity checking.

Parameters:

Name Type Description Default
d AnyDict

Dict to convert into an object

required
kwargs Any

Keyword arguments

  • strict: if True, raise an error if extraneous dict fields are present

{}

Returns:

Type Description
Self

Converted object of this class

Source code in fancy_dataclass/dict.py
@classmethod
def from_dict(cls, d: AnyDict, **kwargs: Any) -> Self:
    """Constructs an object from a dictionary of fields.

    This may also perform some basic type/validity checking.

    Args:
        d: Dict to convert into an object
        kwargs: Keyword arguments <ul><li>`strict`: if `True`, raise an error if extraneous dict fields are present</li></ul>

    Returns:
        Converted object of this class"""
    # first establish the type, which may be present in the 'type' field of the dict
    typename = d.get('type')
    if typename is None:  # type field unspecified, so use the calling class
        tp = cls
    else:
        cls_name = fully_qualified_class_name(cls) if ('.' in typename) else cls.__name__
        if cls_name == typename:  # type name already matches this class
            tp = cls
        else:
            # tp must be a subclass of cls
            # the name must be in scope to be found, allowing two alternatives for retrieval:
            # option 1: all subclasses of this DictDataclass are defined in the same module as the base class
            # option 2: the name is fully qualified, so the name can be loaded into scope
            # call from_dict on the subclass in case it has its own custom implementation
            # (remove the type name before passing to the constructor)
            d2 = {key: val for (key, val) in d.items() if (key != 'type')}
            return cls._get_subclass_with_name(typename).from_dict(d2, **kwargs)
    conv = None
    if cls.__settings__.flattened:
        # produce equivalent subfield-flattened type
        settings = copy(tp.__settings__)
        settings.flattened = True
        conv = _flatten_dataclass(tp, cls.__bases__)[1]
        tp = conv.to_type  # type: ignore[assignment]
        tp.__settings__ = settings
    strict = kwargs.get('strict', False)
    result: Self = tp(**tp._dataclass_args_from_dict(d, strict=strict))
    return conv.backward(result) if cls.__settings__.flattened else result  # type: ignore

to_dict(**kwargs)

Converts the object to a Python dict which, by default, suppresses values matching their dataclass defaults.

Parameters:

Name Type Description Default
kwargs Any

Keyword arguments

  • full: if True (or if the class has suppress_defaults=False), does not suppress default values

{}

Returns:

Type Description
AnyDict

A dict whose keys match the dataclass's fields

Source code in fancy_dataclass/dict.py
def to_dict(self, **kwargs: Any) -> AnyDict:
    """Converts the object to a Python dict which, by default, suppresses values matching their dataclass defaults.

    Args:
        kwargs: Keyword arguments <ul><li>`full`: if `True` (or if the class has `suppress_defaults=False`), does not suppress default values</li></ul>

    Returns:
        A dict whose keys match the dataclass's fields"""
    full = kwargs.get('full', False)
    return self._to_dict(full)

DictDataclassFieldSettings dataclass

Bases: FieldSettings

Settings for DictDataclass fields.

Each field may define a metadata dict containing any of the following entries:

  • suppress: suppress this field in the dict representation
    • Note: if the field is a class variable, it is excluded by default; you can set suppress=False to force the field's inclusion.
  • suppress_default: suppress this field in the dict if it matches its default value (overrides class-level suppress_defaults)
Source code in fancy_dataclass/dict.py
@dataclass
class DictDataclassFieldSettings(FieldSettings):
    """Settings for [`DictDataclass`][fancy_dataclass.dict.DictDataclass] fields.

    Each field may define a `metadata` dict containing any of the following entries:

    - `suppress`: suppress this field in the dict representation
        - Note: if the field is a class variable, it is excluded by default; you can set `suppress=False` to force the field's inclusion.
    - `suppress_default`: suppress this field in the dict if it matches its default value (overrides class-level `suppress_defaults`)"""
    # suppress the field in the dict
    suppress: Optional[bool] = None
    # suppress the field in the dict if its value matches the default
    suppress_default: Optional[bool] = None

DictDataclassSettings dataclass

Bases: DataclassMixinSettings

Class-level settings for the DictDataclass mixin.

Subclasses of DictDataclass may set the following boolean flags as keyword arguments during inheritance:

  • suppress_defaults: suppress default values in its dict
  • store_type: store the object's type in its dict
  • qualified_type: fully qualify the object type's name in its dict
  • flattened: if True, DictDataclass subfields fields will be merged together with the main fields (provided there are no name collisions); otherwise, they are nested
  • validate: if True, attempt to validate data when converting from a dict
Source code in fancy_dataclass/dict.py
@dataclass
class DictDataclassSettings(DataclassMixinSettings):
    """Class-level settings for the [`DictDataclass`][fancy_dataclass.dict.DictDataclass] mixin.

    Subclasses of `DictDataclass` may set the following boolean flags as keyword arguments during inheritance:

    - `suppress_defaults`: suppress default values in its dict
    - `store_type`: store the object's type in its dict
    - `qualified_type`: fully qualify the object type's name in its dict
    - `flattened`: if `True`, [`DictDataclass`][fancy_dataclass.dict.DictDataclass] subfields fields will be merged together with the main fields (provided there are no name collisions); otherwise, they are nested
    - `validate`: if `True`, attempt to validate data when converting from a dict"""
    suppress_defaults: bool = True
    store_type: bool = False
    qualified_type: bool = False
    flattened: bool = False
    validate: bool = True