Skip to content

mixin

DataclassAdaptable

Mixin class providing the ability to convert (adapt) one dataclass type to another.

Source code in fancy_dataclass/mixin.py
class DataclassAdaptable:
    """Mixin class providing the ability to convert (adapt) one dataclass type to another."""

    @classmethod
    def coerce(cls, obj: object) -> Self:
        """Constructs a `DataclassAdaptable` object from the attributes of an arbitrary object.

        Any missing attributes will be set to their default values."""
        return coerce_to_dataclass(cls, obj)

    def adapt_to(self, dest_type: Type[DA]) -> DA:
        """Converts a `DataclassAdaptable` object to another type, `dest_type`.

        By default this will attempt to coerce the fields from the original type to the new type, but subclasses may override the behavior, e.g. to allow field renaming."""
        return dest_type.coerce(self)

adapt_to(dest_type)

Converts a DataclassAdaptable object to another type, dest_type.

By default this will attempt to coerce the fields from the original type to the new type, but subclasses may override the behavior, e.g. to allow field renaming.

Source code in fancy_dataclass/mixin.py
def adapt_to(self, dest_type: Type[DA]) -> DA:
    """Converts a `DataclassAdaptable` object to another type, `dest_type`.

    By default this will attempt to coerce the fields from the original type to the new type, but subclasses may override the behavior, e.g. to allow field renaming."""
    return dest_type.coerce(self)

coerce(obj) classmethod

Constructs a DataclassAdaptable object from the attributes of an arbitrary object.

Any missing attributes will be set to their default values.

Source code in fancy_dataclass/mixin.py
@classmethod
def coerce(cls, obj: object) -> Self:
    """Constructs a `DataclassAdaptable` object from the attributes of an arbitrary object.

    Any missing attributes will be set to their default values."""
    return coerce_to_dataclass(cls, obj)

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[DataclassMixinSettings]]] = None
    __settings__: ClassVar[Optional[DataclassMixinSettings]] = 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 [`DataclassMixinSettings`][fancy_dataclass.mixin.DataclassMixinSettings]), 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.mixin.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) -> 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."""
        _check_field_settings(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 DataclassMixinSettings), 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 [`DataclassMixinSettings`][fancy_dataclass.mixin.DataclassMixinSettings]), 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.mixin.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__() 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.

Source code in fancy_dataclass/mixin.py
@classmethod
def __post_dataclass_wrap__(cls) -> 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."""
    _check_field_settings(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)

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

DataclassMixinSettings

Bases: DataclassAdaptable

Base class for settings to be associated with fancy_dataclass mixins.

Each DataclassMixin class may store a __settings_type__ attribute consisting of a subclass of this class. The settings object will be instantiated as a __settings__ attribute on a mixin subclass when it is defined.

Source code in fancy_dataclass/mixin.py
class DataclassMixinSettings(DataclassAdaptable):
    """Base class for settings to be associated with `fancy_dataclass` mixins.

    Each [`DataclassMixin`][fancy_dataclass.mixin.DataclassMixin] class may store a `__settings_type__` attribute consisting of a subclass of this class. The settings object will be instantiated as a `__settings__` attribute on a mixin subclass when it is defined."""

FieldSettings

Bases: DataclassAdaptable

Class storing a bundle of parameters that will be extracted from dataclass field metadata.

Each DataclassMixin class may store a __field_settings_type__ attribute which is a FieldSettings subclass. This specifies which keys in the field.metadata dictionary are recognized by the mixin class. Other keys will be ignored (unless they are used by other mixin classes).

Source code in fancy_dataclass/mixin.py
class FieldSettings(DataclassAdaptable):
    """Class storing a bundle of parameters that will be extracted from dataclass field metadata.

    Each [`DataclassMixin`][fancy_dataclass.mixin.DataclassMixin] class may store a `__field_settings_type__` attribute which is a `FieldSettings` subclass. This specifies which keys in the `field.metadata` dictionary are recognized by the mixin class. Other keys will be ignored (unless they are used by other mixin classes)."""

    def type_check(self) -> None:
        """Checks that every field on the `FieldSettings` object is the proper type.

        Raises:
            TypeError: If a field is the wrong type"""
        for fld in dataclasses.fields(self):  # type: ignore[arg-type]
            val = getattr(self, fld.name)
            # semi-robust type checking (could still be improved)
            if not _is_instance(val, fld.type):
                raise TypeError(f'expected type {fld.type} for field {fld.name!r}, got {type(val)}')

    @classmethod
    def from_field(cls, field: dataclasses.Field) -> Self:  # type: ignore[type-arg]
        """Constructs a `FieldSettings` object from a [`dataclasses.Field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.Field)'s metadata.

        Raises:
            TypeError: If any field has the wrong type"""
        assert check_dataclass(cls)
        obj: Self = cls(**{key: val for (key, val) in field.metadata.items() if key in cls.__dataclass_fields__})  # type: ignore[assignment]
        obj.type_check()
        return obj

from_field(field) classmethod

Constructs a FieldSettings object from a dataclasses.Field's metadata.

Raises:

Type Description
TypeError

If any field has the wrong type

Source code in fancy_dataclass/mixin.py
@classmethod
def from_field(cls, field: dataclasses.Field) -> Self:  # type: ignore[type-arg]
    """Constructs a `FieldSettings` object from a [`dataclasses.Field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.Field)'s metadata.

    Raises:
        TypeError: If any field has the wrong type"""
    assert check_dataclass(cls)
    obj: Self = cls(**{key: val for (key, val) in field.metadata.items() if key in cls.__dataclass_fields__})  # type: ignore[assignment]
    obj.type_check()
    return obj

type_check()

Checks that every field on the FieldSettings object is the proper type.

Raises:

Type Description
TypeError

If a field is the wrong type

Source code in fancy_dataclass/mixin.py
def type_check(self) -> None:
    """Checks that every field on the `FieldSettings` object is the proper type.

    Raises:
        TypeError: If a field is the wrong type"""
    for fld in dataclasses.fields(self):  # type: ignore[arg-type]
        val = getattr(self, fld.name)
        # semi-robust type checking (could still be improved)
        if not _is_instance(val, fld.type):
            raise TypeError(f'expected type {fld.type} for field {fld.name!r}, got {type(val)}')