1+ from __future__ import annotations
2+
13from abc import ABC , abstractmethod
2- from collections .abc import Iterable , MutableSequence
3- from typing import get_args , Generic , TypeVar , overload
4+ from collections .abc import Iterable , Mapping , MutableMapping , MutableSequence
5+ from typing import Any , Generic , TypeVar , Protocol , get_args , overload , runtime_checkable
46
57from pydantic import ValidationError , TypeAdapter
68from pydantic_core import InitErrorDetails , PydanticCustomError , core_schema
79
10+ from xtl .exceptions .pydantic import OptionsValidationError
11+
812
913T = TypeVar ('T' )
14+ KT = TypeVar ('KT' )
15+ VT = TypeVar ('VT' )
16+
17+
18+ @runtime_checkable
19+ class TypedMutable (Protocol ):
20+
21+ def _validate (self , value : Any ) -> bool : ...
1022
23+ def _serialize (self ) -> Any : ...
1124
12- class TypedIterable (ABC , Generic [T ]):
25+ @staticmethod
26+ def _serialize_value (value : Any ) -> Any :
27+ if isinstance (value , TypedMutable ):
28+ return value ._serialize ()
29+ return value
30+
31+
32+ def _type_name (tp : Any ) -> str :
33+ return getattr (tp , '__name__' , repr (tp ))
34+
35+
36+ class TypedIterable (ABC , TypedMutable , Generic [T ]):
1337
1438 _builtin_type : type
1539 _data : Iterable [T ]
@@ -24,42 +48,39 @@ def __init__(self, item_type: type[T], initial=None):
2448 def _validate (self , value ) -> T :
2549 try :
2650 return self ._adapter .validate_python (value )
27- except (ValidationError , TypeError ) as e :
28- raise ValidationError .from_exception_data (
29- title = f'{ self .__class__ .__name__ } <{ self ._item_type . __name__ } >' ,
51+ except (ValidationError , TypeError ):
52+ exc = ValidationError .from_exception_data (
53+ title = f'{ self .__class__ .__name__ } <{ _type_name ( self ._item_type ) } >' ,
3054 line_errors = [
3155 InitErrorDetails (
3256 type = PydanticCustomError (
3357 'value_error' ,
3458 'Value error, Invalid data type for validation, ' + \
3559 'expected {itemtype} got {valuetype} instead' ,
36- {'itemtype' : self ._item_type . __name__ , 'valuetype' : type (value )}
60+ {'itemtype' : _type_name ( self ._item_type ) , 'valuetype' : _type_name ( type (value ) )}
3761 ),
3862 loc = ('item' ,),
3963 input = value ,
4064 )
4165 ]
42- ) from None
66+ )
67+ raise OptionsValidationError (exc , type (self ).__name__ ) from exc
4368
4469 def _serialize (self ) -> MutableSequence [T ]:
4570 result = self ._builtin_type ()
4671 for item in self :
47- if isinstance (item , TypedIterable ):
48- result .append (item ._serialize ())
49- else :
50- result .append (item )
72+ result .append (self ._serialize_value (item ))
5173 return result
5274
5375 @classmethod
5476 def __get_pydantic_core_schema__ (cls , source_type , handler ):
55- (item_type ,) = get_args (source_type )
56- # Use list schema as the base
77+ (item_type , ) = get_args (source_type )
5778 list_schema = handler .generate_schema (list [item_type ])
5879
59- def validate (value ):
80+ def validate (value : TypedIterable [ T ] ):
6081 return cls (item_type = item_type , initial = value )
6182
62- def serialize (instance ):
83+ def serialize (instance : TypedIterable [ T ] ):
6384 return instance ._serialize ()
6485
6586 return core_schema .no_info_after_validator_function (
@@ -78,12 +99,111 @@ def __contains__(self, item) -> bool:
7899 return item in self ._data
79100
80101 @abstractmethod
81- def extend (self , values ) -> None :
82- ...
102+ def extend (self , values ) -> None : ...
103+
104+ def __repr__ (self ):
105+ return f'{ self .__class__ .__name__ } <{ _type_name (self ._item_type )} >({ self ._data } )'
106+
107+
108+ class TypedMapping (ABC , TypedMutable , Generic [KT , VT ]):
109+
110+ _builtin_type : type
111+ _data : Mapping [KT , VT ]
112+
113+ def __init__ (self , key_type : type [KT ], value_type : type [VT ], initial = None ):
114+ self ._data = self ._builtin_type ()
115+ if not hasattr (key_type , '__hash__' ):
116+ raise TypeError (f'Key type { key_type } is not hashable, cannot be used as a key in '
117+ f'{ type (self ).__name__ } ' )
118+ self ._key_type = key_type
119+ self ._value_type = value_type
120+ self ._key_adapter = TypeAdapter (self ._key_type )
121+ self ._value_adapter = TypeAdapter (self ._value_type )
122+ if initial is not None :
123+ self .update (initial )
124+
125+ def _validate_key (self , value ) -> KT :
126+ try :
127+ return self ._key_adapter .validate_python (value )
128+ except (ValidationError , TypeError ):
129+ from xtl .exceptions .pydantic import OptionsValidationError
130+
131+ exc = ValidationError .from_exception_data (
132+ title = f'{ self .__class__ .__name__ } <{ _type_name (self ._key_type )} , { _type_name (self ._value_type )} >' ,
133+ line_errors = [
134+ InitErrorDetails (
135+ type = PydanticCustomError (
136+ 'value_error' ,
137+ 'Value error, Invalid data type for validation, ' + \
138+ 'expected {keytype} got {valuetype} instead' ,
139+ {'keytype' : _type_name (self ._key_type ), 'valuetype' : _type_name (type (value ))}
140+ ),
141+ loc = ('key' ,),
142+ input = value ,
143+ )
144+ ]
145+ )
146+
147+ raise OptionsValidationError (exc , type (self ).__name__ ) from exc
148+
149+ def _validate_value (self , value ) -> VT :
150+ try :
151+ return self ._value_adapter .validate_python (value )
152+ except (ValidationError , TypeError ):
153+ from xtl .exceptions .pydantic import OptionsValidationError
154+
155+ exc = ValidationError .from_exception_data (
156+ title = f'{ self .__class__ .__name__ } <{ _type_name (self ._key_type )} , { _type_name (self ._value_type )} >' ,
157+ line_errors = [
158+ InitErrorDetails (
159+ type = PydanticCustomError (
160+ 'value_error' ,
161+ 'Value error, Invalid data type for validation, ' + \
162+ 'expected {valuetype} got {inputtype} instead' ,
163+ {'valuetype' : _type_name (self ._value_type ), 'inputtype' : _type_name (type (value ))}
164+ ),
165+ loc = ('value' ,),
166+ input = value ,
167+ )
168+ ]
169+ )
170+ raise OptionsValidationError (exc , type (self ).__name__ ) from exc
171+
172+ def _serialize (self ) -> dict [KT , VT ]:
173+ result = self ._builtin_type ()
174+ for key , value in self ._data .items ():
175+ result [self ._serialize_value (key )] = self ._serialize_value (value )
176+ return result
177+
178+ @classmethod
179+ def __get_pydantic_core_schema__ (cls , source_type , handler ):
180+ key_type , value_type = get_args (source_type )
181+ dict_schema = handler .generate_schema (dict [key_type , value_type ])
182+
183+ def validate (value : TypedMapping [KT , VT ]):
184+ return cls (key_type = key_type , value_type = value_type , initial = value )
185+
186+ def serialize (instance : TypedMapping [KT , VT ]):
187+ return instance ._serialize ()
188+
189+ return core_schema .no_info_after_validator_function (
190+ validate ,
191+ dict_schema ,
192+ serialization = core_schema .plain_serializer_function_ser_schema (
193+ serialize ,
194+ return_schema = dict_schema ,
195+ ),
196+ )
197+
198+ @abstractmethod
199+ def update (self , values : Mapping [KT , VT ]) -> None : ...
83200
84201 def __repr__ (self ):
85- # 'TypedList<ClassName>(values)'
86- return f'{ self .__class__ .__name__ } <{ self ._item_type .__name__ } >({ self ._data } )'
202+ return (
203+ f'{ self .__class__ .__name__ } '
204+ f'<{ _type_name (self ._key_type )} , { _type_name (self ._value_type )} >'
205+ f'({ self ._data } )'
206+ )
87207
88208
89209class TypedList (TypedIterable [T ], MutableSequence [T ]):
@@ -140,3 +260,44 @@ def append(self, value) -> None:
140260 def extend (self , values ) -> None :
141261 self ._data .extend ([self ._validate (v ) for v in values ])
142262
263+
264+ class TypedDict (TypedMapping [KT , VT ], MutableMapping [KT , VT ]):
265+
266+ _builtin_type = dict
267+ _data : dict [KT , VT ]
268+
269+ def __getitem__ (self , key : KT ) -> VT :
270+ return self ._data [key ]
271+
272+ def __setitem__ (self , key : KT , value : VT ) -> None :
273+ self ._data [self ._validate_key (key )] = self ._validate_value (value )
274+
275+ def __delitem__ (self , key : KT ) -> None :
276+ del self ._data [key ]
277+
278+ def __iter__ (self ):
279+ return iter (self ._data )
280+
281+ def __len__ (self ) -> int :
282+ return len (self ._data )
283+
284+ def __contains__ (self , key : KT ) -> bool :
285+ return key in self ._data
286+
287+ def __eq__ (self , other ):
288+ if isinstance (other , TypedMapping ):
289+ return self ._data == other ._data
290+ elif isinstance (other , Mapping ):
291+ return self ._data == dict (other )
292+ else :
293+ raise NotImplementedError
294+
295+ @staticmethod
296+ def _iter_items (values ):
297+ if isinstance (values , MutableMapping ):
298+ return values .items ()
299+ return values
300+
301+ def update (self , values ) -> None :
302+ for key , value in self ._iter_items (values ):
303+ self [key ] = value
0 commit comments