1"""Module-level dataclasses functions."""
2
3from .constants import FIELDS_NAME
4from .decorator import _dataclass
5from .field import Field
6
7try:
8 from typing import Any, Iterable, TypeVar
9
10 T = TypeVar("T")
11except ImportError:
12 pass
13
14
[docs]
15def is_dataclass(obj: object) -> bool:
16 """Check if an object or class is a dataclass."""
17 cls = obj if isinstance(obj, type) else type(obj)
18 return hasattr(cls, FIELDS_NAME)
19
20
[docs]
21def fields(obj: object) -> tuple[Field, ...]:
22 """Retrieve all the Fields of an object or class.
23
24 Fields are returned in alphabetical order by name.
25 """
26 cls = obj if isinstance(obj, type) else type(obj)
27 return tuple(sorted(getattr(cls, FIELDS_NAME).values(), key=lambda f: f.name))
28
29
[docs]
30def replace(obj: T, **changes: Any) -> T:
31 """Create a new object with the specified fields replaced."""
32 fields = getattr(obj, FIELDS_NAME)
33 init_args = {f.name: getattr(obj, f.name) for f in fields.values() if f.init}
34 for name, new_value in changes.items():
35 field = fields.get(name)
36 if not field:
37 raise TypeError(f"Unknown field: {name}")
38 if not field.init:
39 raise ValueError(f"Cannot replace field defined with init=False: {name}")
40 init_args[name] = new_value
41 return (type(obj))(**init_args)
42
43
[docs]
44def astuple(obj: object, *, tuple_factory: Any = tuple) -> Any:
45 """Intentionally unimplemented as we do not preserve field ordering."""
46 raise NotImplementedError("astuple() is intentionally not implemented. ")
47
48
[docs]
49def asdict(
50 obj: object,
51 *,
52 dict_factory: Any = dict,
53) -> Any:
54 """Convert dataclass instance to a dict."""
55 if not is_dataclass(obj):
56 raise TypeError(f"Expected a dataclass, got an object of type {type(obj)}")
57 args: list[tuple[str, Any]] = []
58 for field in fields(obj):
59 name = field.name
60 value = getattr(obj, name)
61 args.append((name, asdict_value(value, dict_factory)))
62 return dict_factory(args)
63
64
65def asdict_value(obj: object, dict_factory: Any) -> Any:
66 """Internal helper for asdict.
67
68 Converts obj into a for storing into asdict entries, recursing to find
69 nested dataclass instances as needed."""
70
71 # Types that can simply be copied over without recursion.
72 simple_types = {int, float, bool, complex, bytes, str, type(None)}
73 if type(obj) in simple_types:
74 return obj
75 if is_dataclass(obj):
76 return asdict(obj, dict_factory=dict_factory)
77 if isinstance(obj, (list, tuple)):
78 return (type(obj))(asdict_value(item, dict_factory) for item in obj)
79 if isinstance(obj, dict):
80 return {
81 asdict_value(key, dict_factory): asdict_value(value, dict_factory)
82 for key, value in obj.items()
83 }
84 raise TypeError(f"Unsupported type: {type(obj)}")
85
86
[docs]
87def make_dataclass(
88 cls_name: str,
89 fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]],
90 *,
91 bases: tuple[type, ...] = (),
92 namespace: dict[str, Any] | None = None,
93 init: bool = True,
94 repr: bool = True,
95 eq: bool = True,
96 order: bool = False,
97 unsafe_hash: bool = False,
98 frozen: bool = False,
99) -> type[Any]:
100 """Dynamically create a dataclass."""
101 # Attributes of dynamically-created class.
102 attrs = dict(**(namespace or {}))
103 for f in fields:
104 # Normalize fields to 3-tuple form.
105 if isinstance(f, str):
106 # str to 3-tuple
107 f = (f, object, Field())
108 if not isinstance(f, tuple):
109 raise TypeError(
110 f"Field specifier must be a str or tuple. Instead got {type(f)}"
111 )
112 if len(f) == 2:
113 # 2-tuple to 3-tuple
114 f = (f[0], object, Field())
115 if len(f) != 3:
116 raise TypeError(
117 f"Field specifier must have length 2 or 3. Instead got {len(f)}"
118 )
119 name, _, field = f
120 attrs[name] = field
121 return _dataclass(
122 type(cls_name, bases, attrs),
123 init=init,
124 repr=repr,
125 eq=eq,
126 order=order,
127 unsafe_hash=unsafe_hash,
128 frozen=frozen,
129 )