Module pedantic.decorators.cls_deco_frozen_dataclass

Expand source code
from copy import deepcopy
from dataclasses import dataclass, fields, replace
from typing import Type, TypeVar, Any, Union, Callable, Dict

from pedantic.get_context import get_context
from pedantic.type_checking_logic.check_types import assert_value_matches_type

T = TypeVar('T')


def frozen_type_safe_dataclass(cls: Type[T]) -> Type[T]:
    """ Shortcut for @frozen_dataclass(type_safe=True) """

    return frozen_dataclass(type_safe=True)(cls)


def frozen_dataclass(
        cls: Type[T] = None,
        type_safe: bool = False,
        order: bool = False,
        kw_only: bool = True,
        slots: bool = False,
) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
    """
        Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)]
        decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below).

        If [type_safe] is True, a type check is performed for each field after the __post_init__ method was called
        which itself s directly called after the __init__ constructor.
        Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only.

        In a nutshell, the followings methods will be added to the decorated class automatically:
        - __init__() gives you a simple constructor like "Foo(a=6, b='hi', c=True)"
        - __eq__() lets you compare objects easily with "a == b"
        - __hash__() is also needed for instance comparison
        - __repr__() gives you a nice output when you call "print(foo)"
        - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters.
        - deep_copy_with() allows you to create deep copies and modify them.
        - validate_types() allows you to validate the types of the dataclass.
                           This is called automatically when [type_safe] is True.

        If the [order] parameter is True (default is False), the following comparison methods
        will be added additionally:
        - __lt__() lets you compare instance like "a < b"
        - __le__() lets you compare instance like "a <= b"
        - __gt__() lets you compare instance like "a > b"
        - __ge__() lets you compare instance like "a >= b"

        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.

        The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10.

        Example:

        >>> @frozen_dataclass
        ... class Foo:
        ...     a: int
        ...     b: str
        ...     c: bool
        >>> foo = Foo(a=6, b='hi', c=True)
        >>> print(foo)
        Foo(a=6, b='hi', c=True)
        >>> print(foo.copy_with())
        Foo(a=6, b='hi', c=True)
        >>> print(foo.copy_with(a=42))
        Foo(a=42, b='hi', c=True)
        >>> print(foo.copy_with(b='Hello'))
        Foo(a=6, b='Hello', c=True)
        >>> print(foo.copy_with(c=False))
        Foo(a=6, b='hi', c=False)
        >>> print(foo.copy_with(a=676676, b='new', c=False))
        Foo(a=676676, b='new', c=False)
    """

    def decorator(cls_: Type[T]) -> Type[T]:
        args = {'frozen': True, 'order': order, 'kw_only': kw_only, 'slots': slots}

        if type_safe:
            old_post_init = getattr(cls_, '__post_init__', lambda _: None)

            def new_post_init(self) -> None:
                old_post_init(self)
                context = get_context(depth=3, increase_depth_if_name_matches=[
                    copy_with.__name__,
                    deep_copy_with.__name__,
                ])
                self.validate_types(_context=context)

            setattr(cls_, '__post_init__', new_post_init)  # must be done before applying dataclass()

        new_class = dataclass(**args)(cls_)  # slots = True will create a new class!

        def copy_with(self, **kwargs: Any) -> T:
            """
                Creates a new immutable instance that by copying all fields of this instance replaced by the new values.
                Keep in mind that this is a shallow copy!
            """

            return replace(self, **kwargs)

        def deep_copy_with(self, **kwargs: Any) -> T:
            """
                Creates a new immutable instance that by deep copying all fields of
                this instance replaced by the new values.
            """

            current_values = {field.name: deepcopy(getattr(self, field.name)) for field in fields(self)}
            return new_class(**{**current_values, **kwargs})

        def validate_types(self, *, _context: Dict[str, Type] = None) -> None:
            """
                Checks that all instance variable have the correct type.
                Raises a [PedanticTypeCheckException] if at least one type is incorrect.
            """

            props = fields(new_class)

            if _context is None:
                # method was called by user
                _context = get_context(depth=2)

            _context = {**_context, **self.__init__.__globals__, self.__class__.__name__: self.__class__}

            for field in props:
                assert_value_matches_type(
                    value=getattr(self, field.name),
                    type_=field.type,
                    err=f'In dataclass "{cls_.__name__}" in field "{field.name}": ',
                    type_vars={},
                    context=_context,
                )

        methods_to_add = [copy_with, deep_copy_with, validate_types]

        for method in methods_to_add:
            setattr(new_class, method.__name__, method)

        return new_class

    if cls is None:
        return decorator

    return decorator(cls_=cls)


if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS)

Functions

def frozen_dataclass(cls: Type[~T] = None, type_safe: bool = False, order: bool = False, kw_only: bool = True, slots: bool = False) ‑> Union[Type[~T], Callable[[Type[~T]], Type[~T]]]

Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)] decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below).

If [type_safe] is True, a type check is performed for each field after the post_init method was called which itself s directly called after the init constructor. Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only.

In a nutshell, the followings methods will be added to the decorated class automatically: - init() gives you a simple constructor like "Foo(a=6, b='hi', c=True)" - eq() lets you compare objects easily with "a == b" - hash() is also needed for instance comparison - repr() gives you a nice output when you call "print(foo)" - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters. - deep_copy_with() allows you to create deep copies and modify them. - validate_types() allows you to validate the types of the dataclass. This is called automatically when [type_safe] is True.

If the [order] parameter is True (default is False), the following comparison methods will be added additionally: - lt() lets you compare instance like "a < b" - le() lets you compare instance like "a <= b" - gt() lets you compare instance like "a > b" - ge() lets you compare instance like "a >= b"

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.

The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10.

Example:

>>> @frozen_dataclass
... class Foo:
...     a: int
...     b: str
...     c: bool
>>> foo = Foo(a=6, b='hi', c=True)
>>> print(foo)
Foo(a=6, b='hi', c=True)
>>> print(foo.copy_with())
Foo(a=6, b='hi', c=True)
>>> print(foo.copy_with(a=42))
Foo(a=42, b='hi', c=True)
>>> print(foo.copy_with(b='Hello'))
Foo(a=6, b='Hello', c=True)
>>> print(foo.copy_with(c=False))
Foo(a=6, b='hi', c=False)
>>> print(foo.copy_with(a=676676, b='new', c=False))
Foo(a=676676, b='new', c=False)
Expand source code
def frozen_dataclass(
        cls: Type[T] = None,
        type_safe: bool = False,
        order: bool = False,
        kw_only: bool = True,
        slots: bool = False,
) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
    """
        Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)]
        decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below).

        If [type_safe] is True, a type check is performed for each field after the __post_init__ method was called
        which itself s directly called after the __init__ constructor.
        Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only.

        In a nutshell, the followings methods will be added to the decorated class automatically:
        - __init__() gives you a simple constructor like "Foo(a=6, b='hi', c=True)"
        - __eq__() lets you compare objects easily with "a == b"
        - __hash__() is also needed for instance comparison
        - __repr__() gives you a nice output when you call "print(foo)"
        - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters.
        - deep_copy_with() allows you to create deep copies and modify them.
        - validate_types() allows you to validate the types of the dataclass.
                           This is called automatically when [type_safe] is True.

        If the [order] parameter is True (default is False), the following comparison methods
        will be added additionally:
        - __lt__() lets you compare instance like "a < b"
        - __le__() lets you compare instance like "a <= b"
        - __gt__() lets you compare instance like "a > b"
        - __ge__() lets you compare instance like "a >= b"

        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.

        The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10.

        Example:

        >>> @frozen_dataclass
        ... class Foo:
        ...     a: int
        ...     b: str
        ...     c: bool
        >>> foo = Foo(a=6, b='hi', c=True)
        >>> print(foo)
        Foo(a=6, b='hi', c=True)
        >>> print(foo.copy_with())
        Foo(a=6, b='hi', c=True)
        >>> print(foo.copy_with(a=42))
        Foo(a=42, b='hi', c=True)
        >>> print(foo.copy_with(b='Hello'))
        Foo(a=6, b='Hello', c=True)
        >>> print(foo.copy_with(c=False))
        Foo(a=6, b='hi', c=False)
        >>> print(foo.copy_with(a=676676, b='new', c=False))
        Foo(a=676676, b='new', c=False)
    """

    def decorator(cls_: Type[T]) -> Type[T]:
        args = {'frozen': True, 'order': order, 'kw_only': kw_only, 'slots': slots}

        if type_safe:
            old_post_init = getattr(cls_, '__post_init__', lambda _: None)

            def new_post_init(self) -> None:
                old_post_init(self)
                context = get_context(depth=3, increase_depth_if_name_matches=[
                    copy_with.__name__,
                    deep_copy_with.__name__,
                ])
                self.validate_types(_context=context)

            setattr(cls_, '__post_init__', new_post_init)  # must be done before applying dataclass()

        new_class = dataclass(**args)(cls_)  # slots = True will create a new class!

        def copy_with(self, **kwargs: Any) -> T:
            """
                Creates a new immutable instance that by copying all fields of this instance replaced by the new values.
                Keep in mind that this is a shallow copy!
            """

            return replace(self, **kwargs)

        def deep_copy_with(self, **kwargs: Any) -> T:
            """
                Creates a new immutable instance that by deep copying all fields of
                this instance replaced by the new values.
            """

            current_values = {field.name: deepcopy(getattr(self, field.name)) for field in fields(self)}
            return new_class(**{**current_values, **kwargs})

        def validate_types(self, *, _context: Dict[str, Type] = None) -> None:
            """
                Checks that all instance variable have the correct type.
                Raises a [PedanticTypeCheckException] if at least one type is incorrect.
            """

            props = fields(new_class)

            if _context is None:
                # method was called by user
                _context = get_context(depth=2)

            _context = {**_context, **self.__init__.__globals__, self.__class__.__name__: self.__class__}

            for field in props:
                assert_value_matches_type(
                    value=getattr(self, field.name),
                    type_=field.type,
                    err=f'In dataclass "{cls_.__name__}" in field "{field.name}": ',
                    type_vars={},
                    context=_context,
                )

        methods_to_add = [copy_with, deep_copy_with, validate_types]

        for method in methods_to_add:
            setattr(new_class, method.__name__, method)

        return new_class

    if cls is None:
        return decorator

    return decorator(cls_=cls)
def frozen_type_safe_dataclass(cls: Type[~T]) ‑> Type[~T]

Shortcut for @frozen_dataclass(type_safe=True)

Expand source code
def frozen_type_safe_dataclass(cls: Type[T]) -> Type[T]:
    """ Shortcut for @frozen_dataclass(type_safe=True) """

    return frozen_dataclass(type_safe=True)(cls)