@@ -455,18 +455,29 @@ def dump_json(filename, data):
455
455
filename(str): Filename for the file.
456
456
data(dict): Data to save to file.
457
457
458
+ Raises:
459
+ KeyboardInterrupt: If the user issued a KeyboardInterrupt.
460
+
458
461
"""
459
462
pathlib .Path (os .path .dirname (filename )).mkdir (parents = True , exist_ok = True )
460
- with open (filename , 'w' ) as f :
463
+ with open (filename , 'w' , encoding = 'utf-8' ) as f :
461
464
# We do our own circular reference handling.
462
465
# Sometimes sort_keys fails because the keys don't get made into
463
466
# strings early enough.
464
- json .dump (data ,
465
- f ,
466
- indent = 2 ,
467
- sort_keys = False ,
468
- cls = LogEncoder ,
469
- check_circular = False )
467
+ # This feature is useful, but causes way too many weird errors.
468
+ # For this reason we catch almost any exception.
469
+ # pylint: disable=broad-except
470
+ try :
471
+ json .dump (data ,
472
+ f ,
473
+ indent = 2 ,
474
+ sort_keys = False ,
475
+ cls = LogEncoder ,
476
+ check_circular = False )
477
+ except KeyboardInterrupt as e :
478
+ raise e
479
+ except Exception :
480
+ pass
470
481
471
482
472
483
def get_metadata ():
@@ -579,44 +590,80 @@ def __init__(self, *args, **kwargs):
579
590
'itertools' ,
580
591
}
581
592
582
- def default (self , o ):
593
+ def default (self , o , path = '' ):
583
594
"""Perform JSON encoding.
584
595
585
596
Args:
586
597
o (object): Object to encode.
587
-
588
- Raises:
589
- TypeError: If `o` cannot be turned into JSON even using `repr(o)`.
598
+ path (str): "Path" to o for describing backreferences.
590
599
591
600
Returns:
592
601
dict or str or float or bool: Object encoded in JSON.
593
602
594
603
"""
595
- # Why is this method hidden? What does that mean?
596
- # pylint: disable=method-hidden
597
- # pylint: disable=too-many-branches
598
- # pylint: disable=too-many-return-statements
599
604
# This circular reference checking code was copied from the standard
600
- # library json implementation, but it outputs a repr'd string instead
601
- # of ValueError on a circular reference.
602
- if isinstance (o , (int , bool , float , str )):
605
+ # library json implementation, but it detects all backreferences
606
+ # instead of just circular ones and outputs a message with the path to
607
+ # the prior reference instead of raising a ValueError
608
+ if isinstance (o , (int , bool , float , str , type (None ))):
603
609
return o
604
610
else :
605
611
markerid = id (o )
606
612
if markerid in self ._markers :
607
- return 'circular ' + repr (o )
613
+ original_path = self ._markers [markerid ]
614
+ return f'reference to { original_path } '
608
615
else :
609
- self ._markers [markerid ] = o
610
- try :
611
- return self ._default_inner (o )
612
- finally :
613
- del self ._markers [markerid ]
616
+ self ._markers [markerid ] = path
617
+ return self ._default_general_cases (o , path )
614
618
615
- def _default_inner (self , o ):
616
- """Perform JSON encoding.
619
+ def _default_general_cases (self , o , path ):
620
+ """Handle JSON encoding among the "general" cases.
621
+
622
+ First, tries to just use the default encoder, then various special
623
+ cases, then by turning o into a list, then by calling repr, and finally
624
+ by returning the string '$invalid'
617
625
618
626
Args:
619
627
o (object): Object to encode.
628
+ path (str): "Path" to o for describing backreferences.
629
+
630
+ Returns:
631
+ dict or str or float or bool: Object encoded in JSON.
632
+
633
+ """
634
+ try :
635
+ return json .JSONEncoder .default (self , o )
636
+ except TypeError :
637
+ pass
638
+ try :
639
+ return self ._default_special_cases (o , path )
640
+ except TypeError :
641
+ pass
642
+ try :
643
+ # This case handles many built-in datatypes like deques
644
+ return [
645
+ self .default (v , f'{ path } /{ i } ' ) for i , v in enumerate (list (o ))
646
+ ]
647
+ except TypeError :
648
+ pass
649
+ try :
650
+ # This case handles most other weird objects.
651
+ return repr (o )
652
+ except TypeError :
653
+ pass
654
+ except ValueError :
655
+ pass
656
+ return '$invalid'
657
+
658
+ def _default_special_cases (self , o , path ):
659
+ """Handle various special cases we frequently want to JSON encode.
660
+
661
+ Note that these cases aren't _that_ special, and include dicts, enums,
662
+ np.numbers, etc.
663
+
664
+ Args:
665
+ o (object): Object to encode.
666
+ path (str): "Path" to o for describing backreferences.
620
667
621
668
Raises:
622
669
TypeError: If `o` cannot be turned into JSON even using `repr(o)`.
@@ -626,70 +673,65 @@ def _default_inner(self, o):
626
673
dict or str or float or bool: Object encoded in JSON.
627
674
628
675
"""
629
- # Why is this method hidden? What does that mean?
630
- # pylint: disable=method-hidden
631
676
# pylint: disable=too-many-branches
632
677
# pylint: disable=too-many-return-statements
633
- # This circular reference checking code was copied from the standard
634
- # library json implementation, but it outputs a repr'd string instead
635
- # of ValueError on a circular reference.
636
- try :
637
- return json .JSONEncoder .default (self , o )
638
- except TypeError as err :
639
- if isinstance (o , dict ):
640
- data = {}
641
- for (k , v ) in o .items ():
642
- if isinstance (k , str ):
643
- data [k ] = self .default (v )
644
- else :
645
- data [repr (k )] = self .default (v )
646
- return data
647
- elif isinstance (o , weakref .ref ):
648
- return repr (o )
649
- elif type (o ).__module__ .split ('.' )[0 ] in self .BLOCKED_MODULES :
650
- return repr (o )
651
- elif isinstance (o , type ):
652
- return {'$typename' : o .__module__ + '.' + o .__name__ }
653
- elif isinstance (o , np .number ):
654
- # For some reason these aren't natively considered
655
- # serializable.
656
- # JSON doesn't actually have ints, so always use a float.
657
- return float (o )
658
- elif isinstance (o , np .bool8 ):
659
- return bool (o )
660
- elif isinstance (o , enum .Enum ):
661
- return {
662
- '$enum' :
663
- o .__module__ + '.' + o .__class__ .__name__ + '.' + o .name
664
- }
665
- elif isinstance (o , np .ndarray ):
666
- return repr (o )
667
- elif hasattr (o , '__dict__' ) or hasattr (o , '__slots__' ):
668
- obj_dict = getattr (o , '__dict__' , None )
669
- if obj_dict is not None :
670
- data = {k : self .default (v ) for (k , v ) in obj_dict .items ()}
671
- else :
672
- data = {
673
- s : self .default (getattr (o , s ))
674
- for s in o .__slots__
675
- }
676
- t = type (o )
677
- data ['$type' ] = t .__module__ + '.' + t .__name__
678
- return data
679
- elif callable (o ) and hasattr (o , '__name__' ):
680
- if getattr (o , '__module__' , None ) is not None :
681
- return {'$function' : o .__module__ + '.' + o .__name__ }
678
+ if isinstance (o , dict ):
679
+ data = {}
680
+ for (k , v ) in o .items ():
681
+ if isinstance (k , str ):
682
+ data [k ] = self .default (v , f'{ path } /{ k } ' )
682
683
else :
683
- return repr (o )
684
+ data [repr (k )] = self .default (v , f'{ path } /{ k !r} ' )
685
+ return data
686
+ elif isinstance (o , weakref .ref ):
687
+ return repr (o )
688
+ elif type (o ).__module__ .split ('.' )[0 ] in self .BLOCKED_MODULES :
689
+ return repr (o )
690
+ elif isinstance (o , type ):
691
+ return {'$typename' : o .__module__ + '.' + o .__name__ }
692
+ elif isinstance (o , np .bool8 ):
693
+ return bool (o )
694
+ elif isinstance (o , np .number ):
695
+ # For some reason these aren't natively considered
696
+ # serializable.
697
+ # JSON doesn't actually have ints, so always use a float.
698
+ # Some strange numpy "number" types can actually be None,
699
+ # so this case can actually fail as well, which will then fall back
700
+ # to one of the general cases.
701
+ return float (o )
702
+ elif isinstance (o , enum .Enum ):
703
+ return {
704
+ '$enum' :
705
+ o .__module__ + '.' + o .__class__ .__name__ + '.' + o .name
706
+ }
707
+ elif isinstance (o , np .ndarray ):
708
+ return repr (o )
709
+ elif hasattr (o , '__dict__' ) or hasattr (o , '__slots__' ):
710
+ obj_dict = getattr (o , '__dict__' , None )
711
+ if obj_dict is not None :
712
+ # Some objects will change their fields while being
713
+ # iterated over, so make a copy of their dictionary.
714
+ obj_dict = obj_dict .copy ()
715
+ data = {
716
+ k : self .default (v , f'{ path } /{ k } ' )
717
+ for (k , v ) in obj_dict .items ()
718
+ # There's a lot of spam from empty dict / list fields
719
+ # The output of this JSONEncoder is not intended to be
720
+ # loaded back into the original objects anyways.
721
+ if not isinstance (v , (list , dict , set , tuple )) or v
722
+ }
684
723
else :
685
- try :
686
- # This case handles many built-in datatypes like deques
687
- return [self .default (v ) for v in list (o )]
688
- except TypeError :
689
- pass
690
- try :
691
- # This case handles most other weird objects.
692
- return repr (o )
693
- except TypeError :
694
- pass
695
- raise err
724
+ data = {
725
+ s : self .default (getattr (o , s ), f'{ path } /{ s } ' )
726
+ for s in o .__slots__
727
+ }
728
+ t = type (o )
729
+ data ['$type' ] = t .__module__ + '.' + t .__name__
730
+ return data
731
+ elif callable (o ) and hasattr (o , '__name__' ):
732
+ if getattr (o , '__module__' , None ) is not None :
733
+ return {'$function' : o .__module__ + '.' + o .__name__ }
734
+ else :
735
+ return repr (o )
736
+ else :
737
+ raise TypeError ('Could not JSON encode object' )
0 commit comments