|
| 1 | +from collections.abc import Callable |
1 | 2 | from copy import deepcopy |
2 | 3 | from dataclasses import dataclass, fields, replace |
3 | | -from typing import Type, TypeVar, Any, Union, Callable, Dict |
| 4 | +from typing import Any, TypeVar |
4 | 5 |
|
5 | 6 | from pedantic.get_context import get_context |
6 | 7 | from pedantic.type_checking_logic.check_types import assert_value_matches_type |
7 | 8 |
|
8 | 9 | T = TypeVar('T') |
9 | 10 |
|
10 | 11 |
|
11 | | -def frozen_type_safe_dataclass(cls: Type[T]) -> Type[T]: |
12 | | - """ Shortcut for @frozen_dataclass(type_safe=True) """ |
| 12 | +def frozen_type_safe_dataclass(cls: type[T]) -> type[T]: |
| 13 | + """Shortcut for @frozen_dataclass(type_safe=True)""" |
13 | 14 |
|
14 | 15 | return frozen_dataclass(type_safe=True)(cls) |
15 | 16 |
|
16 | 17 |
|
17 | | -def frozen_dataclass( |
18 | | - cls: Type[T] = None, |
19 | | - type_safe: bool = False, |
20 | | - order: bool = False, |
21 | | - kw_only: bool = True, |
22 | | - slots: bool = False, |
23 | | -) -> Union[Type[T], Callable[[Type[T]], Type[T]]]: |
| 18 | +def frozen_dataclass( # noqa: C901 |
| 19 | + cls: type[T] | None = None, |
| 20 | + type_safe: bool = False, |
| 21 | + order: bool = False, |
| 22 | + kw_only: bool = True, |
| 23 | + slots: bool = False, |
| 24 | +) -> type[T] | Callable[[type[T]], type[T]]: |
24 | 25 | """ |
25 | | - Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)] |
26 | | - decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below). |
27 | | -
|
28 | | - If [type_safe] is True, a type check is performed for each field after the __post_init__ method was called |
29 | | - which itself s directly called after the __init__ constructor. |
30 | | - Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only. |
31 | | -
|
32 | | - In a nutshell, the followings methods will be added to the decorated class automatically: |
33 | | - - __init__() gives you a simple constructor like "Foo(a=6, b='hi', c=True)" |
34 | | - - __eq__() lets you compare objects easily with "a == b" |
35 | | - - __hash__() is also needed for instance comparison |
36 | | - - __repr__() gives you a nice output when you call "print(foo)" |
37 | | - - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters. |
38 | | - - deep_copy_with() allows you to create deep copies and modify them. |
39 | | - - validate_types() allows you to validate the types of the dataclass. |
40 | | - This is called automatically when [type_safe] is True. |
41 | | -
|
42 | | - If the [order] parameter is True (default is False), the following comparison methods |
43 | | - will be added additionally: |
44 | | - - __lt__() lets you compare instance like "a < b" |
45 | | - - __le__() lets you compare instance like "a <= b" |
46 | | - - __gt__() lets you compare instance like "a > b" |
47 | | - - __ge__() lets you compare instance like "a >= b" |
48 | | -
|
49 | | - These compare the class as if it were a tuple of its fields, in order. |
50 | | - Both instances in the comparison must be of the identical type. |
51 | | -
|
52 | | - The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10. |
53 | | -
|
54 | | - Example: |
55 | | -
|
56 | | - >>> @frozen_dataclass |
57 | | - ... class Foo: |
58 | | - ... a: int |
59 | | - ... b: str |
60 | | - ... c: bool |
61 | | - >>> foo = Foo(a=6, b='hi', c=True) |
62 | | - >>> print(foo) |
63 | | - Foo(a=6, b='hi', c=True) |
64 | | - >>> print(foo.copy_with()) |
65 | | - Foo(a=6, b='hi', c=True) |
66 | | - >>> print(foo.copy_with(a=42)) |
67 | | - Foo(a=42, b='hi', c=True) |
68 | | - >>> print(foo.copy_with(b='Hello')) |
69 | | - Foo(a=6, b='Hello', c=True) |
70 | | - >>> print(foo.copy_with(c=False)) |
71 | | - Foo(a=6, b='hi', c=False) |
72 | | - >>> print(foo.copy_with(a=676676, b='new', c=False)) |
73 | | - Foo(a=676676, b='new', c=False) |
| 26 | + Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)] |
| 27 | + decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below). |
| 28 | +
|
| 29 | + If [type_safe] is True, a type check is performed for each field after the __post_init__ method was called |
| 30 | + which itself s directly called after the __init__ constructor. |
| 31 | + Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only. |
| 32 | +
|
| 33 | + In a nutshell, the followings methods will be added to the decorated class automatically: |
| 34 | + - __init__() gives you a simple constructor like "Foo(a=6, b='hi', c=True)" |
| 35 | + - __eq__() lets you compare objects easily with "a == b" |
| 36 | + - __hash__() is also needed for instance comparison |
| 37 | + - __repr__() gives you a nice output when you call "print(foo)" |
| 38 | + - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters. |
| 39 | + - deep_copy_with() allows you to create deep copies and modify them. |
| 40 | + - validate_types() allows you to validate the types of the dataclass. |
| 41 | + This is called automatically when [type_safe] is True. |
| 42 | +
|
| 43 | + If the [order] parameter is True (default is False), the following comparison methods |
| 44 | + will be added additionally: |
| 45 | + - __lt__() lets you compare instance like "a < b" |
| 46 | + - __le__() lets you compare instance like "a <= b" |
| 47 | + - __gt__() lets you compare instance like "a > b" |
| 48 | + - __ge__() lets you compare instance like "a >= b" |
| 49 | +
|
| 50 | + These compare the class as if it were a tuple of its fields, in order. |
| 51 | + Both instances in the comparison must be of the identical type. |
| 52 | +
|
| 53 | + The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10. |
| 54 | +
|
| 55 | + Example: |
| 56 | + >>> @frozen_dataclass |
| 57 | + ... class Foo: |
| 58 | + ... a: int |
| 59 | + ... b: str |
| 60 | + ... c: bool |
| 61 | + >>> foo = Foo(a=6, b='hi', c=True) |
| 62 | + >>> print(foo) |
| 63 | + Foo(a=6, b='hi', c=True) |
| 64 | + >>> print(foo.copy_with()) |
| 65 | + Foo(a=6, b='hi', c=True) |
| 66 | + >>> print(foo.copy_with(a=42)) |
| 67 | + Foo(a=42, b='hi', c=True) |
| 68 | + >>> print(foo.copy_with(b='Hello')) |
| 69 | + Foo(a=6, b='Hello', c=True) |
| 70 | + >>> print(foo.copy_with(c=False)) |
| 71 | + Foo(a=6, b='hi', c=False) |
| 72 | + >>> print(foo.copy_with(a=676676, b='new', c=False)) |
| 73 | + Foo(a=676676, b='new', c=False) |
74 | 74 | """ |
75 | 75 |
|
76 | | - def decorator(cls_: Type[T]) -> Type[T]: |
| 76 | + def decorator(cls_: type[T]) -> type[T]: |
77 | 77 | args = {'frozen': True, 'order': order, 'kw_only': kw_only, 'slots': slots} |
78 | 78 |
|
79 | 79 | if type_safe: |
80 | 80 | old_post_init = getattr(cls_, '__post_init__', lambda _: None) |
81 | 81 |
|
82 | | - def new_post_init(self) -> None: |
| 82 | + def new_post_init(self: T) -> None: |
83 | 83 | old_post_init(self) |
84 | 84 | context = get_context(depth=3, increase_depth_if_name_matches=[ |
85 | 85 | copy_with.__name__, |
86 | 86 | deep_copy_with.__name__, |
87 | 87 | ]) |
88 | 88 | self.validate_types(_context=context) |
89 | 89 |
|
90 | | - setattr(cls_, '__post_init__', new_post_init) # must be done before applying dataclass() |
| 90 | + cls_.__post_init__ = new_post_init # must be done before applying dataclass() |
91 | 91 |
|
92 | 92 | new_class = dataclass(**args)(cls_) # slots = True will create a new class! |
93 | 93 |
|
94 | | - def copy_with(self, **kwargs: Any) -> T: |
| 94 | + def copy_with(self: T, **kwargs: Any) -> T: |
95 | 95 | """ |
96 | | - Creates a new immutable instance that by copying all fields of this instance replaced by the new values. |
97 | | - Keep in mind that this is a shallow copy! |
| 96 | + Creates a new immutable instance that by copying all fields of this instance replaced by the new values. |
| 97 | + Keep in mind that this is a shallow copy! |
98 | 98 | """ |
99 | 99 |
|
100 | 100 | return replace(self, **kwargs) |
101 | 101 |
|
102 | | - def deep_copy_with(self, **kwargs: Any) -> T: |
| 102 | + def deep_copy_with(self: T, **kwargs: Any) -> T: |
103 | 103 | """ |
104 | | - Creates a new immutable instance that by deep copying all fields of |
105 | | - this instance replaced by the new values. |
| 104 | + Creates a new immutable instance that by deep copying all fields of |
| 105 | + this instance replaced by the new values. |
106 | 106 | """ |
107 | 107 |
|
108 | 108 | current_values = {field.name: deepcopy(getattr(self, field.name)) for field in fields(self)} |
109 | 109 | return new_class(**{**current_values, **kwargs}) |
110 | 110 |
|
111 | | - def validate_types(self, *, _context: Dict[str, Type] = None) -> None: |
| 111 | + def validate_types(self: T, *, _context: dict[str, type] | None = None) -> None: |
112 | 112 | """ |
113 | | - Checks that all instance variable have the correct type. |
114 | | - Raises a [PedanticTypeCheckException] if at least one type is incorrect. |
| 113 | + Checks that all instance variable have the correct type. |
| 114 | + Raises a [PedanticTypeCheckException] if at least one type is incorrect. |
115 | 115 | """ |
116 | 116 |
|
117 | 117 | props = fields(new_class) |
|
0 commit comments