13
13
Any ,
14
14
Callable ,
15
15
Iterator ,
16
- Literal ,
17
16
Optional ,
18
17
Self ,
19
18
Tuple ,
37
36
38
37
from azul import (
39
38
R ,
39
+ cached_property ,
40
40
config ,
41
41
require ,
42
42
)
@@ -177,6 +177,9 @@ def validator(_instance, field, value):
177
177
178
178
type Source = list [str | tuple [str , ...] | Source ]
179
179
180
+ type FromJSON = Callable [[AnyJSON ], Any ]
181
+ type ToJSON = Callable [[Any ], AnyJSON ]
182
+
180
183
181
184
class SerializableAttrs (Serializable , attrs .AttrsInstance ):
182
185
"""
@@ -293,10 +296,6 @@ def _assert_concrete(cls):
293
296
assert not cls ._deferred_fields , R (
294
297
'Class has fields of unknown type' , cls ._deferred_fields )
295
298
296
- class Metadata (TypedDict ):
297
- from_json : Callable [[AnyJSON ], Any ] | None
298
- to_json : Callable [[Any ], AnyJSON ] | None
299
-
300
299
def __init_subclass__ (cls ):
301
300
super ().__init_subclass__ ()
302
301
try :
@@ -365,31 +364,22 @@ def _make(cls, fields: list[attrs.Attribute]) -> frozenset[str]:
365
364
cls ._define (to_json )
366
365
return deferred_fields
367
366
368
- @classmethod
369
- def _serializable (cls ,
370
- field : attrs .Attribute ,
371
- key : Literal ['from_json' , 'to_json' ]
372
- ) -> bool :
373
- try :
374
- return field .metadata ['azul' ][key ] is not None
375
- except KeyError :
376
- return True
377
-
378
367
@classmethod
379
368
def _make_from_json (cls , fields : list [attrs .Attribute ]) -> Callable :
380
369
globals = {cls .__name__ : cls }
370
+ deserializers = (cls .Deserializer (cls , field , globals ) for field in fields )
381
371
source = cls ._indent ([
382
372
'@classmethod' ,
383
373
'def _from_json(cls, json):' , [
384
374
f'kwargs = super({ cls .__name__ } , cls)._from_json(json)' ,
385
375
* flatten (
386
376
[
387
- f'x = json["{ field .name } "]' ,
388
- * (cls . Deserializer ( cls , field , globals ) .handle ('x' )),
389
- f'kwargs["{ field .name } "] = x'
377
+ f'x = json["{ deserializer . field .name } "]' ,
378
+ * (deserializer .handle ('x' )),
379
+ f'kwargs["{ deserializer . field .name } "] = x'
390
380
]
391
- for field in fields
392
- if cls . _serializable ( field , 'from_json' )
381
+ for deserializer in deserializers
382
+ if deserializer . enabled
393
383
),
394
384
'return kwargs'
395
385
]
@@ -399,6 +389,7 @@ def _make_from_json(cls, fields: list[attrs.Attribute]) -> Callable:
399
389
@classmethod
400
390
def _make_to_json (cls , fields : list [attrs .Attribute ]) -> Callable :
401
391
globals = {cls .__name__ : cls }
392
+ serializers = (cls .Serializer (cls , field , globals ) for field in fields )
402
393
to_json = cls ._indent ([
403
394
'def to_json(self):' , [
404
395
# Using the super() shortcut would require messing with the
@@ -407,11 +398,11 @@ def _make_to_json(cls, fields: list[attrs.Attribute]) -> Callable:
407
398
f'json = super({ cls .__name__ } , self).to_json()' ,
408
399
* flatten (
409
400
[
410
- f'x = self.{ field .name } ' ,
411
- f'json["{ field .name } "] = ' + cls . Serializer ( cls , field , globals ) .handle ('x' )
401
+ f'x = self.{ serializer . field .name } ' ,
402
+ f'json["{ serializer . field .name } "] = ' + serializer .handle ('x' )
412
403
]
413
- for field in fields
414
- if cls . _serializable ( field , 'to_json' )
404
+ for serializer in serializers
405
+ if serializer . enabled
415
406
),
416
407
'return json'
417
408
]
@@ -478,13 +469,25 @@ class Strategy[T](metaclass=ABCMeta):
478
469
class MustDefer (Exception ):
479
470
pass
480
471
481
- def handle (self , x : str ) -> T :
472
+ class Custom (TypedDict ):
473
+ from_json : FromJSON | None
474
+ to_json : ToJSON | None
475
+
476
+ @cached_property
477
+ def custom (self ) -> Custom | None :
478
+ return self ._metadata ('custom' , None )
479
+
480
+ def _metadata [V ](self , key : str , default : V ) -> V :
482
481
try :
483
- metadata = self .field .metadata ['azul' ]
482
+ return self .field .metadata ['azul' ][ key ]
484
483
except KeyError :
484
+ return default
485
+
486
+ def handle (self , x : str ) -> T :
487
+ if self .custom is None :
485
488
return self ._handle (x , self ._reify (self .field .type ))
486
489
else :
487
- return self ._custom (x , metadata )
490
+ return self ._custom (x )
488
491
489
492
def _owner (self ) -> type :
490
493
"""
@@ -551,6 +554,11 @@ def _handle(self, x: str, field_type: Any):
551
554
return self ._dict (x , key_type , value_type )
552
555
raise TypeError ('Unserializable field' , field_type , self .field )
553
556
557
+ @property
558
+ @abstractmethod
559
+ def enabled (self ) -> bool :
560
+ raise NotImplementedError
561
+
554
562
@abstractmethod
555
563
def _primitive (self , x : str , field_type : type ) -> T :
556
564
raise NotImplementedError
@@ -576,11 +584,15 @@ def _dict(self, x: str, key_type: type, value_type: type) -> T:
576
584
raise NotImplementedError
577
585
578
586
@abstractmethod
579
- def _custom (self , x : str , metadata : 'SerializableAttrs.Metadata' ) -> T :
587
+ def _custom (self , x : str ) -> T :
580
588
raise NotImplementedError
581
589
582
590
class Deserializer (Strategy [Source ]):
583
591
592
+ @property
593
+ def enabled (self ) -> bool :
594
+ return self .custom is None or self .custom ['from_json' ] is not None
595
+
584
596
def _optional (self , x : str , field_type : type ) -> Source :
585
597
return [
586
598
f'if { x } is not None:' , self ._handle (x , field_type )
@@ -632,15 +644,20 @@ def _dict(self, x: str, key_type: type, value_type: type) -> Source:
632
644
f'{ x } = { d } '
633
645
]
634
646
635
- def _custom (self , x : str , metadata : 'SerializableAttrs.Metadata' ) -> Source :
647
+ def _custom (self , x : str ) -> Source :
636
648
var_name = self .field .name + '_from_json'
637
- self .globals [var_name ] = not_none (metadata ['from_json' ])
649
+ from_json = not_none (not_none (self .custom )['from_json' ])
650
+ self .globals [var_name ] = from_json
638
651
return [
639
652
f'{ x } = { var_name } ({ x } )'
640
653
]
641
654
642
655
class Serializer (Strategy [str ]):
643
656
657
+ @property
658
+ def enabled (self ) -> bool :
659
+ return self .custom is None or self .custom ['to_json' ] is not None
660
+
644
661
def _primitive (self , x : str , field_type : type ) -> str :
645
662
return x
646
663
@@ -665,32 +682,34 @@ def _dict(self, x: str, key_type: type, value_type: type) -> str:
665
682
k_ , v_ = self ._handle (k , key_type ), self ._handle (v , value_type )
666
683
return f'{{{ k_ } : { v_ } for { k } , { v } in x.items()}}'
667
684
668
- def _custom (self , x : str , metadata : 'SerializableAttrs.Metadata' ) -> str :
685
+ def _custom (self , x : str ) -> str :
686
+ to_json = not_none (not_none (self .custom )['to_json' ])
669
687
var_name = self .field .name + '_to_json'
670
- self .globals [var_name ] = not_none ( metadata [ ' to_json' ])
688
+ self .globals [var_name ] = to_json
671
689
return f'{ var_name } ({ x } )'
672
690
673
691
674
- def serializable [T : attrs .Attribute ](field : T ,
675
- from_json : Callable [[AnyJSON ], Any ],
676
- to_json : Callable [[Any ], AnyJSON ]) -> T :
692
+ def serializable [T : attrs .Attribute ](field : T | None = None ,
693
+ * ,
694
+ from_json : FromJSON ,
695
+ to_json : ToJSON ) -> T :
677
696
"""
678
697
Use the provided callables to (de)serialize values of the given field,
679
698
instead of generating them.
680
699
681
700
>>> @attrs.frozen
682
701
... class Foo(SerializableAttrs):
683
- ... x: set[str] = serializable(attrs.field(), to_json=sorted, from_json=set)
702
+ ... x: set[str] = serializable(to_json=sorted, from_json=set)
684
703
685
704
>>> Foo(x={'b','a'}).to_json()
686
705
{'x': ['a', 'b']}
687
706
688
707
>>> Foo.from_json({'x': ['a']})
689
708
Foo(x={'a'})
690
709
"""
691
- field . metadata [ 'azul' ] = SerializableAttrs .Metadata (from_json = from_json ,
692
- to_json = to_json )
693
- return field
710
+ custom = SerializableAttrs .Strategy . Custom (from_json = from_json ,
711
+ to_json = to_json )
712
+ return _set_field_metadata ( field , 'custom' , custom )
694
713
695
714
696
715
def not_serializable [T : attrs .Attribute ](field : T ) -> T :
@@ -710,6 +729,14 @@ def not_serializable[T: attrs.Attribute](field: T) -> T:
710
729
>>> Foo.from_json({})
711
730
Foo(x=42)
712
731
"""
713
- field .metadata ['azul' ] = SerializableAttrs .Metadata (from_json = None ,
714
- to_json = None )
732
+ custom = SerializableAttrs .Strategy .Custom (from_json = None ,
733
+ to_json = None )
734
+ return _set_field_metadata (field , 'custom' , custom )
735
+
736
+
737
+ def _set_field_metadata [T : attrs .Attribute ](field : T | None , key , value ):
738
+ if field is None :
739
+ field = attrs .field ()
740
+ metadata = field .metadata .setdefault ('azul' , {})
741
+ metadata [key ] = value
715
742
return field
0 commit comments