Module pedantic.tests.test_frozen_dataclass

Expand source code
import unittest
from abc import ABC
from dataclasses import dataclass, FrozenInstanceError
from typing import List, Dict, Set, Tuple, Awaitable, Callable, Generic, TypeVar, Optional

from pedantic.decorators.cls_deco_frozen_dataclass import frozen_dataclass, frozen_type_safe_dataclass
from pedantic.exceptions import PedanticTypeCheckException


@frozen_dataclass
class Foo:
    a: int
    b: str
    c: bool


@frozen_type_safe_dataclass
class B:
    v: Set[int]


@frozen_type_safe_dataclass
class A:
    foo: List[int]
    bar: Dict[str, str]
    values: Tuple[B, B]


class TestFrozenDataclass(unittest.TestCase):
    def test_equals_and_hash(self):
        a = Foo(a=6, b='hi', c=True)
        b = Foo(a=6, b='hi', c=True)
        c = Foo(a=7, b='hi', c=True)

        self.assertEqual(a, b)
        self.assertEqual(hash(a), hash(b))

        self.assertNotEqual(a, c)
        self.assertNotEqual(hash(a), hash(c))

    def test_copy_with(self):
        foo = Foo(a=6, b='hi', c=True)

        copy_1 = foo.copy_with()
        self.assertEqual(foo, copy_1)

        copy_2 = foo.copy_with(a=42)
        self.assertNotEqual(foo, copy_2)
        self.assertEqual(42, copy_2.a)
        self.assertEqual(foo.b, copy_2.b)
        self.assertEqual(foo.c, copy_2.c)

        copy_3 = foo.copy_with(b='Hello')
        self.assertNotEqual(foo, copy_3)
        self.assertEqual(foo.a, copy_3.a)
        self.assertEqual('Hello', copy_3.b)
        self.assertEqual(foo.c, copy_3.c)

        copy_4 = foo.copy_with(c=False)
        self.assertNotEqual(foo, copy_4)
        self.assertEqual(foo.a, copy_4.a)
        self.assertEqual(foo.b, copy_4.b)
        self.assertEqual(False, copy_4.c)

        copy_5 = foo.copy_with(a=676676, b='new', c=False)
        self.assertNotEqual(foo, copy_5)
        self.assertEqual(676676, copy_5.a)
        self.assertEqual('new', copy_5.b)
        self.assertEqual(False, copy_5.c)

    def test_validate_types(self):
        foo = Foo(a=6, b='hi', c=True)
        foo.validate_types()

        bar = Foo(a=6.6, b='hi', c=True)

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            bar.validate_types()

        expected = 'In dataclass "Foo" in field "a": Type hint is incorrect: Argument 6.6 of type <class \'float\'> ' \
                   'does not match expected type <class \'int\'>.'
        self.assertEqual(str(exc.exception), expected)

    def test_frozen_dataclass_above_dataclass(self):
        # This is the same behavior like
        # >>> @dataclass(frozen=True)
        # ... @dataclass
        # ... class C:
        # ...     foo: int

        @frozen_dataclass
        @dataclass
        class A:
            foo: int

        with self.assertRaises(expected_exception=TypeError):
            A()

        with self.assertRaises(expected_exception=FrozenInstanceError):
            A(foo=3)

    def test_frozen_dataclass_below_dataclass(self):
        @dataclass
        @frozen_dataclass
        class A:
            foo: int

        with self.assertRaises(expected_exception=TypeError):
            A()

        a = A(foo=3)

        with self.assertRaises(expected_exception=FrozenInstanceError):
            a.foo = 4

        b = a.copy_with(foo=4)
        self.assertEqual(4, b.foo)


    def test_frozen_typesafe_dataclass_with_post_init(self):
        b = 3

        @frozen_dataclass(type_safe=True)
        class A:
            foo: int

            def __post_init__(self) -> None:
                nonlocal b
                b = 33

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            A(foo=42.7)

        self.assertEqual(
            'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
            ' <class \'float\'> does not match expected type <class \'int\'>.',
            str(exc.exception)
        )

        # we check that the __post_init__ method is executed
        self.assertEqual(33, b)

    def test_frozen_typesafe_dataclass_without_post_init(self):
        @frozen_dataclass(type_safe=True)
        class A:
            foo: int

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            A(foo=42.7)

        self.assertEqual(
            'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
            ' <class \'float\'> does not match expected type <class \'int\'>.',
            str(exc.exception)
        )

    def test_frozen_dataclass_with_empty_braces(self):
        @frozen_dataclass()
        class A:
            foo: int

        a = A(foo=42)
        self.assertEqual(42, a.foo)

    def test_frozen_dataclass_no_braces(self):
        @frozen_dataclass
        class A:
            foo: int

        a = A(foo=42)
        self.assertEqual(42, a.foo)

    def test_frozen_dataclass_order(self):
        @frozen_dataclass(order=True)
        class A:
            foo: int
            bar: int

        a = A(foo=42, bar=43)
        b = A(foo=42, bar=42)
        c = A(foo=41, bar=44)
        d = A(foo=44, bar=0)
        self.assertLess(b, a)
        self.assertLess(c, b)
        self.assertLess(a, d)

    def test_frozen_type_safe_dataclass_copy_with_check(self):
        @frozen_type_safe_dataclass
        class A:
            foo: int
            bar: bool

        a = A(foo=42, bar=False)

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            a.copy_with(foo=1.1)

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            a.copy_with(bar=11)

        a.copy_with(foo=11, bar=True)

    def test_copy_with_is_shallow(self):
        a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
        shallow = a.copy_with()

        # manipulation
        shallow.bar['hello'] = 'pedantic'
        shallow.foo.append(3)

        self.assertEqual([1, 2, 3], a.foo)
        self.assertEqual([1, 2, 3], shallow.foo)
        self.assertEqual('pedantic', a.bar['hello'])
        self.assertEqual('pedantic', shallow.bar['hello'])

    def test_copy_with_and_deep_copy_with(self):
        a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
        deep = a.deep_copy_with()

        # manipulation
        deep.bar['hello'] = 'pedantic'
        deep.foo.append(3)

        self.assertEqual([1, 2], a.foo)
        self.assertEqual([1, 2, 3], deep.foo)
        self.assertEqual('world', a.bar['hello'])
        self.assertEqual('pedantic', deep.bar['hello'])

    def test_frozen_dataclass_inheritance_override_post_init(self):
        i = 1

        @frozen_type_safe_dataclass
        class A:
            bar: int

            def __post_init__(self):
                nonlocal i
                i += 1
                print('hello a')

        @frozen_type_safe_dataclass
        class B(A):
            foo: int

            def __post_init__(self):
                nonlocal i
                i *= 10
                print('hello b')

        A(bar=3)
        self.assertEqual(2, i)

        b = B(bar=3, foo=42)
        self.assertEqual(20, i)  # post init of A was not called
        self.assertEqual(3, b.bar)
        self.assertEqual(42, b.foo)

        a = b.copy_with()
        self.assertEqual(b, a)
        self.assertEqual(200, i)

    def test_frozen_dataclass_inheritance_not_override_post_init(self):
        i = 1

        @frozen_type_safe_dataclass
        class A:
            bar: int

            def __post_init__(self):
                nonlocal i
                i += 1
                print('hello a')

        @frozen_type_safe_dataclass
        class B(A):
            foo: int

        A(bar=3)
        self.assertEqual(2, i)

        b = B(bar=3, foo=42)
        self.assertEqual(3, i)  # post init of A was called
        self.assertEqual(3, b.bar)
        self.assertEqual(42, b.foo)

        a = b.copy_with()
        self.assertEqual(b, a)
        self.assertEqual(4, i)

    def test_type_safe_frozen_dataclass_with_awaitable(self):
        @frozen_type_safe_dataclass
        class A:
            f: Callable[..., Awaitable[int]]

        async def _cb() -> int:
            return 42

        async def _cb_2() -> str:
            return '42'

        A(f=_cb)
        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            A(f=_cb_2)

    def test_type_safe_frozen_dataclass_with_forward_ref(self):
        T = TypeVar('T')

        class State(Generic[T], ABC):
            pass

        class StateMachine(Generic[T], ABC):
            pass

        @frozen_type_safe_dataclass
        class StateChangeResult:
            new_state: Optional['MachineState']

        class MachineState(State['MachineStateMachine']):
            pass

        class OfflineMachineState(MachineState):
            pass

        class OnlineMachineState:
            pass

        class MachineStateMachine(StateMachine[MachineState]):
            pass

        s = StateChangeResult(new_state=OfflineMachineState())

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            StateChangeResult(new_state=OnlineMachineState())

        s.validate_types()

    def test_forward_ref_to_itself(self):
        """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

        @frozen_type_safe_dataclass
        class Comment:
            replies: List['Comment']

        comment = Comment(replies=[Comment(replies=[])])
        comment.copy_with(replies=[Comment(replies=[])])
        comment.validate_types()

    def test_forward_ref_to_itself_while_class_not_in_scope(self):
        """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

        def _scope():
            @frozen_type_safe_dataclass
            class Comment:
                replies: List['Comment']

            def _make(replies=None):
                return Comment(replies=replies or [])

            return _make

        make = _scope()

        comment = make(replies=[make(replies=[])])
        comment.copy_with(replies=[make(replies=[])])
        comment.validate_types()

    def test_slots_work_with_equals(self):
        @frozen_dataclass(slots=True)
        class Foo:
            a: int

        o = Foo(a=0)
        assert o == o.copy_with()

Classes

class A (*, foo: List[int], bar: Dict[str, str], values: Tuple[BB])

A(*, foo: List[int], bar: Dict[str, str], values: Tuple[pedantic.tests.test_frozen_dataclass.B, pedantic.tests.test_frozen_dataclass.B])

Expand source code
@frozen_type_safe_dataclass
class A:
    foo: List[int]
    bar: Dict[str, str]
    values: Tuple[B, B]

Class variables

var bar : Dict[str, str]
var foo : List[int]
var values : Tuple[BB]

Methods

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!

Expand source code
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.

Expand source code
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) ‑> None

Checks that all instance variable have the correct type. Raises a [PedanticTypeCheckException] if at least one type is incorrect.

Expand source code
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,
        )
class B (*, v: Set[int])

B(*, v: Set[int])

Expand source code
@frozen_type_safe_dataclass
class B:
    v: Set[int]

Class variables

var v : Set[int]

Methods

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!

Expand source code
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.

Expand source code
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) ‑> None

Checks that all instance variable have the correct type. Raises a [PedanticTypeCheckException] if at least one type is incorrect.

Expand source code
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,
        )
class Foo (*, a: int, b: str, c: bool)

Foo(*, a: int, b: str, c: bool)

Expand source code
@frozen_dataclass
class Foo:
    a: int
    b: str
    c: bool

Class variables

var a : int
var b : str
var c : bool

Methods

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!

Expand source code
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.

Expand source code
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) ‑> None

Checks that all instance variable have the correct type. Raises a [PedanticTypeCheckException] if at least one type is incorrect.

Expand source code
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,
        )
class TestFrozenDataclass (methodName='runTest')

A class whose instances are single test cases.

By default, the test code itself should be placed in a method named 'runTest'.

If the fixture may be used for many test cases, create as many test methods as are needed. When instantiating such a TestCase subclass, specify in the constructor arguments the name of the test method that the instance is to execute.

Test authors should subclass TestCase for their own tests. Construction and deconstruction of the test's environment ('fixture') can be implemented by overriding the 'setUp' and 'tearDown' methods respectively.

If it is necessary to override the init method, the base class init method must always be called. It is important that subclasses should not change the signature of their init method, since instances of the classes are instantiated automatically by parts of the framework in order to be run.

When subclassing TestCase, you can set these attributes: * failureException: determines which exception will be raised when the instance's assertion methods fail; test methods raising this exception will be deemed to have 'failed' rather than 'errored'. * longMessage: determines whether long messages (including repr of objects used in assert methods) will be printed on failure in addition to any explicit message passed. * maxDiff: sets the maximum length of a diff in failure messages by assert methods using difflib. It is looked up as an instance attribute so can be configured by individual tests if required.

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestFrozenDataclass(unittest.TestCase):
    def test_equals_and_hash(self):
        a = Foo(a=6, b='hi', c=True)
        b = Foo(a=6, b='hi', c=True)
        c = Foo(a=7, b='hi', c=True)

        self.assertEqual(a, b)
        self.assertEqual(hash(a), hash(b))

        self.assertNotEqual(a, c)
        self.assertNotEqual(hash(a), hash(c))

    def test_copy_with(self):
        foo = Foo(a=6, b='hi', c=True)

        copy_1 = foo.copy_with()
        self.assertEqual(foo, copy_1)

        copy_2 = foo.copy_with(a=42)
        self.assertNotEqual(foo, copy_2)
        self.assertEqual(42, copy_2.a)
        self.assertEqual(foo.b, copy_2.b)
        self.assertEqual(foo.c, copy_2.c)

        copy_3 = foo.copy_with(b='Hello')
        self.assertNotEqual(foo, copy_3)
        self.assertEqual(foo.a, copy_3.a)
        self.assertEqual('Hello', copy_3.b)
        self.assertEqual(foo.c, copy_3.c)

        copy_4 = foo.copy_with(c=False)
        self.assertNotEqual(foo, copy_4)
        self.assertEqual(foo.a, copy_4.a)
        self.assertEqual(foo.b, copy_4.b)
        self.assertEqual(False, copy_4.c)

        copy_5 = foo.copy_with(a=676676, b='new', c=False)
        self.assertNotEqual(foo, copy_5)
        self.assertEqual(676676, copy_5.a)
        self.assertEqual('new', copy_5.b)
        self.assertEqual(False, copy_5.c)

    def test_validate_types(self):
        foo = Foo(a=6, b='hi', c=True)
        foo.validate_types()

        bar = Foo(a=6.6, b='hi', c=True)

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            bar.validate_types()

        expected = 'In dataclass "Foo" in field "a": Type hint is incorrect: Argument 6.6 of type <class \'float\'> ' \
                   'does not match expected type <class \'int\'>.'
        self.assertEqual(str(exc.exception), expected)

    def test_frozen_dataclass_above_dataclass(self):
        # This is the same behavior like
        # >>> @dataclass(frozen=True)
        # ... @dataclass
        # ... class C:
        # ...     foo: int

        @frozen_dataclass
        @dataclass
        class A:
            foo: int

        with self.assertRaises(expected_exception=TypeError):
            A()

        with self.assertRaises(expected_exception=FrozenInstanceError):
            A(foo=3)

    def test_frozen_dataclass_below_dataclass(self):
        @dataclass
        @frozen_dataclass
        class A:
            foo: int

        with self.assertRaises(expected_exception=TypeError):
            A()

        a = A(foo=3)

        with self.assertRaises(expected_exception=FrozenInstanceError):
            a.foo = 4

        b = a.copy_with(foo=4)
        self.assertEqual(4, b.foo)


    def test_frozen_typesafe_dataclass_with_post_init(self):
        b = 3

        @frozen_dataclass(type_safe=True)
        class A:
            foo: int

            def __post_init__(self) -> None:
                nonlocal b
                b = 33

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            A(foo=42.7)

        self.assertEqual(
            'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
            ' <class \'float\'> does not match expected type <class \'int\'>.',
            str(exc.exception)
        )

        # we check that the __post_init__ method is executed
        self.assertEqual(33, b)

    def test_frozen_typesafe_dataclass_without_post_init(self):
        @frozen_dataclass(type_safe=True)
        class A:
            foo: int

        with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
            A(foo=42.7)

        self.assertEqual(
            'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
            ' <class \'float\'> does not match expected type <class \'int\'>.',
            str(exc.exception)
        )

    def test_frozen_dataclass_with_empty_braces(self):
        @frozen_dataclass()
        class A:
            foo: int

        a = A(foo=42)
        self.assertEqual(42, a.foo)

    def test_frozen_dataclass_no_braces(self):
        @frozen_dataclass
        class A:
            foo: int

        a = A(foo=42)
        self.assertEqual(42, a.foo)

    def test_frozen_dataclass_order(self):
        @frozen_dataclass(order=True)
        class A:
            foo: int
            bar: int

        a = A(foo=42, bar=43)
        b = A(foo=42, bar=42)
        c = A(foo=41, bar=44)
        d = A(foo=44, bar=0)
        self.assertLess(b, a)
        self.assertLess(c, b)
        self.assertLess(a, d)

    def test_frozen_type_safe_dataclass_copy_with_check(self):
        @frozen_type_safe_dataclass
        class A:
            foo: int
            bar: bool

        a = A(foo=42, bar=False)

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            a.copy_with(foo=1.1)

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            a.copy_with(bar=11)

        a.copy_with(foo=11, bar=True)

    def test_copy_with_is_shallow(self):
        a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
        shallow = a.copy_with()

        # manipulation
        shallow.bar['hello'] = 'pedantic'
        shallow.foo.append(3)

        self.assertEqual([1, 2, 3], a.foo)
        self.assertEqual([1, 2, 3], shallow.foo)
        self.assertEqual('pedantic', a.bar['hello'])
        self.assertEqual('pedantic', shallow.bar['hello'])

    def test_copy_with_and_deep_copy_with(self):
        a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
        deep = a.deep_copy_with()

        # manipulation
        deep.bar['hello'] = 'pedantic'
        deep.foo.append(3)

        self.assertEqual([1, 2], a.foo)
        self.assertEqual([1, 2, 3], deep.foo)
        self.assertEqual('world', a.bar['hello'])
        self.assertEqual('pedantic', deep.bar['hello'])

    def test_frozen_dataclass_inheritance_override_post_init(self):
        i = 1

        @frozen_type_safe_dataclass
        class A:
            bar: int

            def __post_init__(self):
                nonlocal i
                i += 1
                print('hello a')

        @frozen_type_safe_dataclass
        class B(A):
            foo: int

            def __post_init__(self):
                nonlocal i
                i *= 10
                print('hello b')

        A(bar=3)
        self.assertEqual(2, i)

        b = B(bar=3, foo=42)
        self.assertEqual(20, i)  # post init of A was not called
        self.assertEqual(3, b.bar)
        self.assertEqual(42, b.foo)

        a = b.copy_with()
        self.assertEqual(b, a)
        self.assertEqual(200, i)

    def test_frozen_dataclass_inheritance_not_override_post_init(self):
        i = 1

        @frozen_type_safe_dataclass
        class A:
            bar: int

            def __post_init__(self):
                nonlocal i
                i += 1
                print('hello a')

        @frozen_type_safe_dataclass
        class B(A):
            foo: int

        A(bar=3)
        self.assertEqual(2, i)

        b = B(bar=3, foo=42)
        self.assertEqual(3, i)  # post init of A was called
        self.assertEqual(3, b.bar)
        self.assertEqual(42, b.foo)

        a = b.copy_with()
        self.assertEqual(b, a)
        self.assertEqual(4, i)

    def test_type_safe_frozen_dataclass_with_awaitable(self):
        @frozen_type_safe_dataclass
        class A:
            f: Callable[..., Awaitable[int]]

        async def _cb() -> int:
            return 42

        async def _cb_2() -> str:
            return '42'

        A(f=_cb)
        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            A(f=_cb_2)

    def test_type_safe_frozen_dataclass_with_forward_ref(self):
        T = TypeVar('T')

        class State(Generic[T], ABC):
            pass

        class StateMachine(Generic[T], ABC):
            pass

        @frozen_type_safe_dataclass
        class StateChangeResult:
            new_state: Optional['MachineState']

        class MachineState(State['MachineStateMachine']):
            pass

        class OfflineMachineState(MachineState):
            pass

        class OnlineMachineState:
            pass

        class MachineStateMachine(StateMachine[MachineState]):
            pass

        s = StateChangeResult(new_state=OfflineMachineState())

        with self.assertRaises(expected_exception=PedanticTypeCheckException):
            StateChangeResult(new_state=OnlineMachineState())

        s.validate_types()

    def test_forward_ref_to_itself(self):
        """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

        @frozen_type_safe_dataclass
        class Comment:
            replies: List['Comment']

        comment = Comment(replies=[Comment(replies=[])])
        comment.copy_with(replies=[Comment(replies=[])])
        comment.validate_types()

    def test_forward_ref_to_itself_while_class_not_in_scope(self):
        """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

        def _scope():
            @frozen_type_safe_dataclass
            class Comment:
                replies: List['Comment']

            def _make(replies=None):
                return Comment(replies=replies or [])

            return _make

        make = _scope()

        comment = make(replies=[make(replies=[])])
        comment.copy_with(replies=[make(replies=[])])
        comment.validate_types()

    def test_slots_work_with_equals(self):
        @frozen_dataclass(slots=True)
        class Foo:
            a: int

        o = Foo(a=0)
        assert o == o.copy_with()

Ancestors

  • unittest.case.TestCase

Methods

def test_copy_with(self)
Expand source code
def test_copy_with(self):
    foo = Foo(a=6, b='hi', c=True)

    copy_1 = foo.copy_with()
    self.assertEqual(foo, copy_1)

    copy_2 = foo.copy_with(a=42)
    self.assertNotEqual(foo, copy_2)
    self.assertEqual(42, copy_2.a)
    self.assertEqual(foo.b, copy_2.b)
    self.assertEqual(foo.c, copy_2.c)

    copy_3 = foo.copy_with(b='Hello')
    self.assertNotEqual(foo, copy_3)
    self.assertEqual(foo.a, copy_3.a)
    self.assertEqual('Hello', copy_3.b)
    self.assertEqual(foo.c, copy_3.c)

    copy_4 = foo.copy_with(c=False)
    self.assertNotEqual(foo, copy_4)
    self.assertEqual(foo.a, copy_4.a)
    self.assertEqual(foo.b, copy_4.b)
    self.assertEqual(False, copy_4.c)

    copy_5 = foo.copy_with(a=676676, b='new', c=False)
    self.assertNotEqual(foo, copy_5)
    self.assertEqual(676676, copy_5.a)
    self.assertEqual('new', copy_5.b)
    self.assertEqual(False, copy_5.c)
def test_copy_with_and_deep_copy_with(self)
Expand source code
def test_copy_with_and_deep_copy_with(self):
    a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
    deep = a.deep_copy_with()

    # manipulation
    deep.bar['hello'] = 'pedantic'
    deep.foo.append(3)

    self.assertEqual([1, 2], a.foo)
    self.assertEqual([1, 2, 3], deep.foo)
    self.assertEqual('world', a.bar['hello'])
    self.assertEqual('pedantic', deep.bar['hello'])
def test_copy_with_is_shallow(self)
Expand source code
def test_copy_with_is_shallow(self):
    a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6})))
    shallow = a.copy_with()

    # manipulation
    shallow.bar['hello'] = 'pedantic'
    shallow.foo.append(3)

    self.assertEqual([1, 2, 3], a.foo)
    self.assertEqual([1, 2, 3], shallow.foo)
    self.assertEqual('pedantic', a.bar['hello'])
    self.assertEqual('pedantic', shallow.bar['hello'])
def test_equals_and_hash(self)
Expand source code
def test_equals_and_hash(self):
    a = Foo(a=6, b='hi', c=True)
    b = Foo(a=6, b='hi', c=True)
    c = Foo(a=7, b='hi', c=True)

    self.assertEqual(a, b)
    self.assertEqual(hash(a), hash(b))

    self.assertNotEqual(a, c)
    self.assertNotEqual(hash(a), hash(c))
def test_forward_ref_to_itself(self)
Expand source code
def test_forward_ref_to_itself(self):
    """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

    @frozen_type_safe_dataclass
    class Comment:
        replies: List['Comment']

    comment = Comment(replies=[Comment(replies=[])])
    comment.copy_with(replies=[Comment(replies=[])])
    comment.validate_types()
def test_forward_ref_to_itself_while_class_not_in_scope(self)
Expand source code
def test_forward_ref_to_itself_while_class_not_in_scope(self):
    """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """

    def _scope():
        @frozen_type_safe_dataclass
        class Comment:
            replies: List['Comment']

        def _make(replies=None):
            return Comment(replies=replies or [])

        return _make

    make = _scope()

    comment = make(replies=[make(replies=[])])
    comment.copy_with(replies=[make(replies=[])])
    comment.validate_types()
def test_frozen_dataclass_above_dataclass(self)
Expand source code
def test_frozen_dataclass_above_dataclass(self):
    # This is the same behavior like
    # >>> @dataclass(frozen=True)
    # ... @dataclass
    # ... class C:
    # ...     foo: int

    @frozen_dataclass
    @dataclass
    class A:
        foo: int

    with self.assertRaises(expected_exception=TypeError):
        A()

    with self.assertRaises(expected_exception=FrozenInstanceError):
        A(foo=3)
def test_frozen_dataclass_below_dataclass(self)
Expand source code
def test_frozen_dataclass_below_dataclass(self):
    @dataclass
    @frozen_dataclass
    class A:
        foo: int

    with self.assertRaises(expected_exception=TypeError):
        A()

    a = A(foo=3)

    with self.assertRaises(expected_exception=FrozenInstanceError):
        a.foo = 4

    b = a.copy_with(foo=4)
    self.assertEqual(4, b.foo)
def test_frozen_dataclass_inheritance_not_override_post_init(self)
Expand source code
def test_frozen_dataclass_inheritance_not_override_post_init(self):
    i = 1

    @frozen_type_safe_dataclass
    class A:
        bar: int

        def __post_init__(self):
            nonlocal i
            i += 1
            print('hello a')

    @frozen_type_safe_dataclass
    class B(A):
        foo: int

    A(bar=3)
    self.assertEqual(2, i)

    b = B(bar=3, foo=42)
    self.assertEqual(3, i)  # post init of A was called
    self.assertEqual(3, b.bar)
    self.assertEqual(42, b.foo)

    a = b.copy_with()
    self.assertEqual(b, a)
    self.assertEqual(4, i)
def test_frozen_dataclass_inheritance_override_post_init(self)
Expand source code
def test_frozen_dataclass_inheritance_override_post_init(self):
    i = 1

    @frozen_type_safe_dataclass
    class A:
        bar: int

        def __post_init__(self):
            nonlocal i
            i += 1
            print('hello a')

    @frozen_type_safe_dataclass
    class B(A):
        foo: int

        def __post_init__(self):
            nonlocal i
            i *= 10
            print('hello b')

    A(bar=3)
    self.assertEqual(2, i)

    b = B(bar=3, foo=42)
    self.assertEqual(20, i)  # post init of A was not called
    self.assertEqual(3, b.bar)
    self.assertEqual(42, b.foo)

    a = b.copy_with()
    self.assertEqual(b, a)
    self.assertEqual(200, i)
def test_frozen_dataclass_no_braces(self)
Expand source code
def test_frozen_dataclass_no_braces(self):
    @frozen_dataclass
    class A:
        foo: int

    a = A(foo=42)
    self.assertEqual(42, a.foo)
def test_frozen_dataclass_order(self)
Expand source code
def test_frozen_dataclass_order(self):
    @frozen_dataclass(order=True)
    class A:
        foo: int
        bar: int

    a = A(foo=42, bar=43)
    b = A(foo=42, bar=42)
    c = A(foo=41, bar=44)
    d = A(foo=44, bar=0)
    self.assertLess(b, a)
    self.assertLess(c, b)
    self.assertLess(a, d)
def test_frozen_dataclass_with_empty_braces(self)
Expand source code
def test_frozen_dataclass_with_empty_braces(self):
    @frozen_dataclass()
    class A:
        foo: int

    a = A(foo=42)
    self.assertEqual(42, a.foo)
def test_frozen_type_safe_dataclass_copy_with_check(self)
Expand source code
def test_frozen_type_safe_dataclass_copy_with_check(self):
    @frozen_type_safe_dataclass
    class A:
        foo: int
        bar: bool

    a = A(foo=42, bar=False)

    with self.assertRaises(expected_exception=PedanticTypeCheckException):
        a.copy_with(foo=1.1)

    with self.assertRaises(expected_exception=PedanticTypeCheckException):
        a.copy_with(bar=11)

    a.copy_with(foo=11, bar=True)
def test_frozen_typesafe_dataclass_with_post_init(self)
Expand source code
def test_frozen_typesafe_dataclass_with_post_init(self):
    b = 3

    @frozen_dataclass(type_safe=True)
    class A:
        foo: int

        def __post_init__(self) -> None:
            nonlocal b
            b = 33

    with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
        A(foo=42.7)

    self.assertEqual(
        'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
        ' <class \'float\'> does not match expected type <class \'int\'>.',
        str(exc.exception)
    )

    # we check that the __post_init__ method is executed
    self.assertEqual(33, b)
def test_frozen_typesafe_dataclass_without_post_init(self)
Expand source code
def test_frozen_typesafe_dataclass_without_post_init(self):
    @frozen_dataclass(type_safe=True)
    class A:
        foo: int

    with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
        A(foo=42.7)

    self.assertEqual(
        'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type'
        ' <class \'float\'> does not match expected type <class \'int\'>.',
        str(exc.exception)
    )
def test_slots_work_with_equals(self)
Expand source code
def test_slots_work_with_equals(self):
    @frozen_dataclass(slots=True)
    class Foo:
        a: int

    o = Foo(a=0)
    assert o == o.copy_with()
def test_type_safe_frozen_dataclass_with_awaitable(self)
Expand source code
def test_type_safe_frozen_dataclass_with_awaitable(self):
    @frozen_type_safe_dataclass
    class A:
        f: Callable[..., Awaitable[int]]

    async def _cb() -> int:
        return 42

    async def _cb_2() -> str:
        return '42'

    A(f=_cb)
    with self.assertRaises(expected_exception=PedanticTypeCheckException):
        A(f=_cb_2)
def test_type_safe_frozen_dataclass_with_forward_ref(self)
Expand source code
def test_type_safe_frozen_dataclass_with_forward_ref(self):
    T = TypeVar('T')

    class State(Generic[T], ABC):
        pass

    class StateMachine(Generic[T], ABC):
        pass

    @frozen_type_safe_dataclass
    class StateChangeResult:
        new_state: Optional['MachineState']

    class MachineState(State['MachineStateMachine']):
        pass

    class OfflineMachineState(MachineState):
        pass

    class OnlineMachineState:
        pass

    class MachineStateMachine(StateMachine[MachineState]):
        pass

    s = StateChangeResult(new_state=OfflineMachineState())

    with self.assertRaises(expected_exception=PedanticTypeCheckException):
        StateChangeResult(new_state=OnlineMachineState())

    s.validate_types()
def test_validate_types(self)
Expand source code
def test_validate_types(self):
    foo = Foo(a=6, b='hi', c=True)
    foo.validate_types()

    bar = Foo(a=6.6, b='hi', c=True)

    with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc:
        bar.validate_types()

    expected = 'In dataclass "Foo" in field "a": Type hint is incorrect: Argument 6.6 of type <class \'float\'> ' \
               'does not match expected type <class \'int\'>.'
    self.assertEqual(str(exc.exception), expected)