Skip to content

mixin

DataclassMixin

Mixin class for adding some kind functionality to a dataclass.

For example, this could provide features for conversion to/from JSON (JSONDataclass), the ability to construct CLI argument parsers (ArgparseDataclass), etc.

This mixin also provides a wrap_dataclass decorator which can be used to wrap an existing dataclass type into one that provides the mixin's functionality.

Source code in fancy_dataclass/mixin.py
class DataclassMixin:
    """Mixin class for adding some kind functionality to a dataclass.

    For example, this could provide features for conversion to/from JSON ([`JSONDataclass`][fancy_dataclass.json.JSONDataclass]), the ability to construct CLI argument parsers ([`ArgparseDataclass`][fancy_dataclass.cli.ArgparseDataclass]), etc.

    This mixin also provides a [`wrap_dataclass`][fancy_dataclass.mixin.DataclassMixin.wrap_dataclass] decorator which can be used to wrap an existing dataclass type into one that provides the mixin's functionality."""

    __settings_type__: ClassVar[Optional[Type[MixinSettings]]] = None
    __settings__: ClassVar[Optional[MixinSettings]] = None
    __field_settings_type__: ClassVar[Optional[Type[FieldSettings]]] = None

    @classmethod
    def __init_subclass__(cls, **kwargs: Any) -> None:
        """When inheriting from this class, you may pass various keyword arguments after the list of base classes.

        If the base class has a `__settings_type__` class attribute (subclass of [`MixinSettings`][fancy_dataclass.settings.MixinSettings]), that class will be instantiated with the provided arguments and stored as a `__settings__` attribute on the subclass. These settings can be used to customize the behavior of the subclass.

        Additionally, the mixin may set the `__field_settings_type__` class attribute to indicate the type (subclass of [`FieldSettings`][fancy_dataclass.settings.FieldSettings]) that should be used for field settings, which are extracted from each field's `metadata` dict."""
        super().__init_subclass__()
        _configure_mixin_settings(cls, **kwargs)
        _configure_field_settings_type(cls)

    @classmethod
    def __post_dataclass_wrap__(cls, wrapped_cls: Type[Self]) -> None:
        """A hook that is called after the [`dataclasses.dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) decorator is applied to the mixin subclass.

        This can be used, for instance, to validate the dataclass fields at definition time.

        NOTE: this function should be _idempotent_, meaning it can be called multiple times with the same effect. This is because it will be called for every base class of the `dataclass`-wrapped class, which may result in duplicate calls.

        Args:
            wrapped_cls: Class wrapped by the `dataclass` decorator"""
        _check_field_settings(wrapped_cls)

    @classmethod
    def _field_settings(cls, field: dataclasses.Field) -> FieldSettings:  # type: ignore[type-arg]
        """Gets the class-specific FieldSettings extracted from the metadata stored on a Field object."""
        stype = cls.__field_settings_type__ or FieldSettings
        return stype.from_field(field)

    @classmethod
    def wrap_dataclass(cls: Type[Self], tp: Type[T], **kwargs: Any) -> Type[Self]:
        """Wraps a dataclass type into a new one which inherits from this mixin class and is otherwise the same.

        Args:
            tp: A dataclass type
            kwargs: Keyword arguments to type constructor

        Returns:
            New dataclass type inheriting from the mixin

        Raises:
            TypeError: If the given type is not a dataclass"""
        check_dataclass(tp)
        if issubclass(tp, cls):  # the type is already a subclass of this one, so just return it
            return tp
        # otherwise, create a new type that inherits from this class
        try:
            return type(tp.__name__, (tp, cls), {}, **kwargs)
        except TypeError as e:
            if 'Cannot create a consistent' in str(e):
                # try the opposite order of inheritance
                return type(tp.__name__, (cls, tp), {}, **kwargs)
            raise

    def _replace(self, **kwargs: Any) -> Self:
        """Constructs a new object with the provided fields modified.

        Args:
            **kwargs: Dataclass fields to modify

        Returns:
            New object with selected fields modified

        Raises:
            TypeError: If an invalid dataclass field is provided"""
        assert hasattr(self, '__dataclass_fields__'), f'{obj_class_name(self)} is not a dataclass type'
        d = {fld.name: getattr(self, fld.name) for fld in dataclasses.fields(self)}  # type: ignore[arg-type]
        for (key, val) in kwargs.items():
            if key in d:
                d[key] = val
            else:
                raise TypeError(f'{key!r} is not a valid field for {obj_class_name(self)}')
        return self.__class__(**d)

    @classmethod
    def get_subclass_with_name(cls, typename: str) -> Type[Self]:
        """Gets the subclass of this class with the given name.

        Args:
            typename: Name of subclass

        Returns:
            Subclass with the given name

        Raises:
            TypeError: If no subclass with the given name exists"""
        return get_subclass_with_name(cls, typename)

__init_subclass__(**kwargs) classmethod

When inheriting from this class, you may pass various keyword arguments after the list of base classes.

If the base class has a __settings_type__ class attribute (subclass of MixinSettings), that class will be instantiated with the provided arguments and stored as a __settings__ attribute on the subclass. These settings can be used to customize the behavior of the subclass.

Additionally, the mixin may set the __field_settings_type__ class attribute to indicate the type (subclass of FieldSettings) that should be used for field settings, which are extracted from each field's metadata dict.

Source code in fancy_dataclass/mixin.py
@classmethod
def __init_subclass__(cls, **kwargs: Any) -> None:
    """When inheriting from this class, you may pass various keyword arguments after the list of base classes.

    If the base class has a `__settings_type__` class attribute (subclass of [`MixinSettings`][fancy_dataclass.settings.MixinSettings]), that class will be instantiated with the provided arguments and stored as a `__settings__` attribute on the subclass. These settings can be used to customize the behavior of the subclass.

    Additionally, the mixin may set the `__field_settings_type__` class attribute to indicate the type (subclass of [`FieldSettings`][fancy_dataclass.settings.FieldSettings]) that should be used for field settings, which are extracted from each field's `metadata` dict."""
    super().__init_subclass__()
    _configure_mixin_settings(cls, **kwargs)
    _configure_field_settings_type(cls)

__post_dataclass_wrap__(wrapped_cls) classmethod

A hook that is called after the dataclasses.dataclass decorator is applied to the mixin subclass.

This can be used, for instance, to validate the dataclass fields at definition time.

NOTE: this function should be idempotent, meaning it can be called multiple times with the same effect. This is because it will be called for every base class of the dataclass-wrapped class, which may result in duplicate calls.

Parameters:

Name Type Description Default
wrapped_cls Type[Self]

Class wrapped by the dataclass decorator

required
Source code in fancy_dataclass/mixin.py
@classmethod
def __post_dataclass_wrap__(cls, wrapped_cls: Type[Self]) -> None:
    """A hook that is called after the [`dataclasses.dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) decorator is applied to the mixin subclass.

    This can be used, for instance, to validate the dataclass fields at definition time.

    NOTE: this function should be _idempotent_, meaning it can be called multiple times with the same effect. This is because it will be called for every base class of the `dataclass`-wrapped class, which may result in duplicate calls.

    Args:
        wrapped_cls: Class wrapped by the `dataclass` decorator"""
    _check_field_settings(wrapped_cls)

_replace(**kwargs)

Constructs a new object with the provided fields modified.

Parameters:

Name Type Description Default
**kwargs Any

Dataclass fields to modify

{}

Returns:

Type Description
Self

New object with selected fields modified

Raises:

Type Description
TypeError

If an invalid dataclass field is provided

Source code in fancy_dataclass/mixin.py
def _replace(self, **kwargs: Any) -> Self:
    """Constructs a new object with the provided fields modified.

    Args:
        **kwargs: Dataclass fields to modify

    Returns:
        New object with selected fields modified

    Raises:
        TypeError: If an invalid dataclass field is provided"""
    assert hasattr(self, '__dataclass_fields__'), f'{obj_class_name(self)} is not a dataclass type'
    d = {fld.name: getattr(self, fld.name) for fld in dataclasses.fields(self)}  # type: ignore[arg-type]
    for (key, val) in kwargs.items():
        if key in d:
            d[key] = val
        else:
            raise TypeError(f'{key!r} is not a valid field for {obj_class_name(self)}')
    return self.__class__(**d)

get_subclass_with_name(typename) classmethod

Gets the subclass of this class with the given name.

Parameters:

Name Type Description Default
typename str

Name of subclass

required

Returns:

Type Description
Type[Self]

Subclass with the given name

Raises:

Type Description
TypeError

If no subclass with the given name exists

Source code in fancy_dataclass/mixin.py
@classmethod
def get_subclass_with_name(cls, typename: str) -> Type[Self]:
    """Gets the subclass of this class with the given name.

    Args:
        typename: Name of subclass

    Returns:
        Subclass with the given name

    Raises:
        TypeError: If no subclass with the given name exists"""
    return get_subclass_with_name(cls, typename)

wrap_dataclass(tp, **kwargs) classmethod

Wraps a dataclass type into a new one which inherits from this mixin class and is otherwise the same.

Parameters:

Name Type Description Default
tp Type[T]

A dataclass type

required
kwargs Any

Keyword arguments to type constructor

{}

Returns:

Type Description
Type[Self]

New dataclass type inheriting from the mixin

Raises:

Type Description
TypeError

If the given type is not a dataclass

Source code in fancy_dataclass/mixin.py
@classmethod
def wrap_dataclass(cls: Type[Self], tp: Type[T], **kwargs: Any) -> Type[Self]:
    """Wraps a dataclass type into a new one which inherits from this mixin class and is otherwise the same.

    Args:
        tp: A dataclass type
        kwargs: Keyword arguments to type constructor

    Returns:
        New dataclass type inheriting from the mixin

    Raises:
        TypeError: If the given type is not a dataclass"""
    check_dataclass(tp)
    if issubclass(tp, cls):  # the type is already a subclass of this one, so just return it
        return tp
    # otherwise, create a new type that inherits from this class
    try:
        return type(tp.__name__, (tp, cls), {}, **kwargs)
    except TypeError as e:
        if 'Cannot create a consistent' in str(e):
            # try the opposite order of inheritance
            return type(tp.__name__, (cls, tp), {}, **kwargs)
        raise