1- from typing import TYPE_CHECKING , Callable , List , Type , TypeVar
1+ from typing import Callable , List , Type , TypeVar
22from typing_extensions import Self , dataclass_transform
33
44import dataclasses
55from dataclasses import asdict , dataclass , is_dataclass
66
77from .dc import get_etypes_for_dc
8- from .exacting import json as ejson # type: ignore
8+ from .exacting import bytes_to_py , json_to_py , jsonc_to_py , py_to_bytes
9+ from .types import NOTHING
910
1011T = TypeVar ("T" , bound = Type )
1112
@@ -32,93 +33,58 @@ def get_exact_init(dc: Type) -> Callable:
3233 etypes = get_etypes_for_dc (dc )
3334 dc .__exact_types__ = etypes
3435
35- def init (self , * args , ** kwargs ):
36- covered = len (args ) + len (kwargs )
37-
38- if covered != len (etypes ):
39- raise ValueError (
40- f"(dataclass { dc .__name__ !r} ) Expected to cover { len (etypes )} parameter(s), but got { covered } "
41- )
42-
43- if args :
44- keys_map = list (etypes ) # keys
45-
46- for idx , value in enumerate (args ):
47- k = keys_map [idx ]
48- res = etypes [keys_map [idx ]].validate (value )
49- if res .has_error ():
50- raise TypeError (
51- get_exact_error_message (
52- [
53- f"During validation of dataclass { dc .__name__ !r} at attribute { k !r} , a type error occurred:" ,
54- * res .errors ,
55- ]
56- )
36+ def init (self , ** kwargs ):
37+ for key , value in etypes .items ():
38+ provided_value = kwargs .get (key , NOTHING )
39+ if provided_value is NOTHING :
40+ field = dc .__dataclass_fields__ [key ]
41+ if field .default is not dataclasses .MISSING :
42+ provided_value = field .default
43+ elif field .default_factory is not dataclasses .MISSING :
44+ provided_value = field .default_factory ()
45+ else :
46+ raise ValueError (
47+ f"Error while validating dataclass { dc .__name__ !r} at attribute { key !r} :\n "
48+ "Field has no value provided."
5749 )
58- setattr (self , k , res .ok )
59-
60- for key , value in kwargs .items ():
61- res = etypes [key ].validate (value )
62- if res .has_error ():
63- raise TypeError (
64- get_exact_error_message (
65- [
66- f"During validation of dataclass { dc .__name__ !r} at attribute { key !r} , a type error occurred:" ,
67- * res .errors ,
68- ]
69- )
50+
51+ res = value .validate (provided_value )
52+ if res .has_error ():
53+ raise TypeError (
54+ get_exact_error_message (
55+ [
56+ f"Error while validating dataclass { dc .__name__ !r} at attribute { key !r} :" ,
57+ * res .errors ,
58+ ]
7059 )
71- setattr ( self , key , res . ok )
60+ )
7261
73- else :
74- for key , value in kwargs .items ():
75- res = etypes [key ].validate (value )
76- if res .has_error ():
77- raise TypeError (
78- get_exact_error_message (
79- [
80- f"During validation of dataclass { dc .__name__ !r} at attribute { key !r} , a type error occurred:" ,
81- * res .errors ,
82- ]
62+ target = res .ok
63+ validators = dc .__dataclass_fields__ [key ].metadata .get (
64+ "exacting_validators"
65+ )
66+ if validators is not None :
67+ for validator in validators :
68+ res_t = validator .validate (target )
69+ if res_t .has_error ():
70+ raise TypeError (
71+ get_exact_error_message (
72+ [
73+ f"Error while validating dataclass { dc .__name__ !r} at attribute { key !r} (field validator):" ,
74+ * res_t .errors ,
75+ ]
76+ )
8377 )
84- )
85- setattr (self , key , res .ok )
78+ target = res_t .ok
79+
80+ setattr (self , key , target )
8681
8782 return None # required!
8883
8984 return init
9085
9186
92- def _patch ():
93- original = dataclasses .dataclass
94-
95- def new (item = None , ** kwargs ):
96- if item is None :
97-
98- def wrapper (t ):
99- x = original (t )
100- init = get_exact_init (x )
101- setattr (x , "__init__" , init )
102- return x
103-
104- return wrapper
105- else :
106- x = dataclass (item )
107- init = get_exact_init (x )
108- setattr (x , "__init__" , init )
109- return x
110-
111- return new
112-
113-
114- # sneaky trick
115- if TYPE_CHECKING :
116- exact = dataclasses .dataclass
117- else :
118- exact = _patch ()
119-
120-
121- @dataclass_transform ()
87+ @dataclass_transform (kw_only_default = True )
12288class _ModelKwOnly : ...
12389
12490
@@ -128,7 +94,8 @@ class Exact(_ModelKwOnly):
12894 """
12995
13096 def __init_subclass__ (cls ) -> None :
131- setattr (cls , "__init__" , get_exact_init (dataclass (cls )))
97+ init = get_exact_init (dataclass (cls ))
98+ setattr (cls , "__init__" , init )
13299
133100 def exact_as_dict (self ):
134101 """(exacting) Creates a dictionary representation of this dataclass instance.
@@ -139,6 +106,10 @@ def exact_as_dict(self):
139106 assert is_dataclass (self )
140107 return asdict (self )
141108
109+ def exact_as_bytes (self ) -> bytes :
110+ """(exacting) Convert this instance of dataclass model into bytes."""
111+ return py_to_bytes (self .exact_as_dict ())
112+
142113 @classmethod
143114 def exact_from_json (cls , raw : str , * , strict : bool = True ) -> Self :
144115 """(exacting) Initialize this dataclass model from JSON.
@@ -178,8 +149,13 @@ class Person(Exact):
178149 strict (bool): Whether to use strict mode.
179150 """
180151 if strict :
181- data = ejson . json_to_py (raw )
152+ data = json_to_py (raw )
182153 else :
183- data = ejson . jsonc_to_py (raw )
154+ data = jsonc_to_py (raw )
184155
185156 return cls (** data )
157+
158+ @classmethod
159+ def exact_from_bytes (cls , raw : bytes ) -> Self :
160+ data = bytes_to_py (raw )
161+ return cls (** data )
0 commit comments