@@ -832,6 +832,154 @@ def from_json(cls, as_json):
832832 def _to_untrimmed_dict (self , in_cls = None ) -> dict :
833833 return self .data
834834
835+ @staticmethod
836+ def trim_iterable (untrimmed ,
837+ to_json = False ,
838+ context : str = None ):
839+ """Convert any :class:`EnforcedNullType` values in ``untrimmed`` to ``'null'``.
840+
841+ :param untrimmed: The iterable whose members may still be
842+ :obj:`None <python:None>` or Python objects.
843+ :type untrimmed: iterable
844+
845+ :param to_json: If ``True``, will remove all members from ``untrimmed`` that are
846+ not serializable to JSON. Defaults to ``False``.
847+ :type to_json: :class:`bool <python:bool>`
848+
849+ :param context: If provided, will inform the method of the context in which it is
850+ being run which may inform special handling cases (e.g. where empty strings may
851+ be important / allowable). Defaults to :obj:`None <python:None>`.
852+ :type context: :class:`str <python:str>` or :obj:`None <python:None>`
853+
854+ :rtype: iterable
855+ """
856+ if not checkers .is_iterable (untrimmed , forbid_literals = (str , bytes , dict )):
857+ return untrimmed
858+
859+ trimmed = []
860+ for item in untrimmed :
861+ if checkers .is_type (item , 'CallbackFunction' ) and to_json :
862+ continue
863+ elif item is None or item == constants .EnforcedNull :
864+ trimmed .append ('null' )
865+ elif hasattr (item , 'trim_dict' ):
866+ updated_context = item .__class__ .__name__
867+ untrimmed_item = item ._to_untrimmed_dict ()
868+ item_as_dict = HighchartsMeta .trim_dict (untrimmed_item ,
869+ to_json = to_json ,
870+ context = updated_context )
871+ if item_as_dict :
872+ trimmed .append (item_as_dict )
873+ elif isinstance (item , dict ):
874+ if item :
875+ trimmed .append (HighchartsMeta .trim_dict (item ,
876+ to_json = to_json ,
877+ context = context ))
878+ elif checkers .is_iterable (item , forbid_literals = (str , bytes , dict )):
879+ if item :
880+ trimmed .append (HighchartsMeta .trim_iterable (item ,
881+ to_json = to_json ,
882+ context = context ))
883+ else :
884+ trimmed .append (item )
885+
886+ return trimmed
887+
888+ @staticmethod
889+ def trim_dict (untrimmed : dict ,
890+ to_json : bool = False ,
891+ context : str = None ) -> dict :
892+ """Remove keys from ``untrimmed`` whose values are :obj:`None <python:None>` and
893+ convert values that have ``.to_dict()`` methods.
894+
895+ :param untrimmed: The :class:`dict <python:dict>` whose values may still be
896+ :obj:`None <python:None>` or Python objects.
897+ :type untrimmed: :class:`dict <python:dict>`
898+
899+ :param to_json: If ``True``, will remove all keys from ``untrimmed`` that are not
900+ serializable to JSON. Defaults to ``False``.
901+ :type to_json: :class:`bool <python:bool>`
902+
903+ :param context: If provided, will inform the method of the context in which it is
904+ being run which may inform special handling cases (e.g. where empty strings may
905+ be important / allowable). Defaults to :obj:`None <python:None>`.
906+ :type context: :class:`str <python:str>` or :obj:`None <python:None>`
907+
908+ :returns: Trimmed :class:`dict <python:dict>`
909+ :rtype: :class:`dict <python:dict>`
910+ """
911+ as_dict = {}
912+ for key in untrimmed :
913+ context_key = f'{ context } .{ key } '
914+ value = untrimmed .get (key , None )
915+ # bool -> Boolean
916+ if isinstance (value , bool ):
917+ as_dict [key ] = value
918+ # Callback Function
919+ elif checkers .is_type (value , 'CallbackFunction' ) and to_json :
920+ continue
921+ # HighchartsMeta -> dict --> object
922+ elif value and hasattr (value , '_to_untrimmed_dict' ):
923+ untrimmed_value = value ._to_untrimmed_dict ()
924+ updated_context = value .__class__ .__name__
925+ trimmed_value = HighchartsMeta .trim_dict (untrimmed_value ,
926+ to_json = to_json ,
927+ context = updated_context )
928+ if trimmed_value :
929+ as_dict [key ] = trimmed_value
930+ # Enforced null
931+ elif isinstance (value , constants .EnforcedNullType ):
932+ if to_json :
933+ as_dict [key ] = None
934+ else :
935+ as_dict [key ] = value
936+ # dict -> object
937+ elif isinstance (value , dict ):
938+ trimmed_value = HighchartsMeta .trim_dict (value ,
939+ to_json = to_json ,
940+ context = context )
941+ if trimmed_value :
942+ as_dict [key ] = trimmed_value
943+ # iterable -> array
944+ elif checkers .is_iterable (value , forbid_literals = (str , bytes , dict )):
945+ trimmed_value = HighchartsMeta .trim_iterable (value ,
946+ to_json = to_json ,
947+ context = context )
948+ if trimmed_value :
949+ as_dict [key ] = trimmed_value
950+ # Datetime or Datetime-like
951+ elif checkers .is_datetime (value ):
952+ trimmed_value = value
953+ if to_json :
954+ if not value .tzinfo :
955+ trimmed_value = value .replace (tzinfo = datetime .timezone .utc )
956+ as_dict [key ] = trimmed_value .timestamp () * 1000
957+ elif hasattr (trimmed_value , 'to_pydatetime' ):
958+ as_dict [key ] = trimmed_value .to_pydatetime ()
959+ else :
960+ as_dict [key ] = trimmed_value
961+ # Date or Time
962+ elif checkers .is_date (value ) or checkers .is_time (value ):
963+ if to_json :
964+ as_dict [key ] = value .isoformat ()
965+ else :
966+ as_dict [key ] = value
967+ # other truthy -> str / number
968+ elif value :
969+ trimmed_value = HighchartsMeta .trim_iterable (value ,
970+ to_json = to_json ,
971+ context = context )
972+ if trimmed_value :
973+ as_dict [key ] = trimmed_value
974+ # other falsy -> str / number
975+ elif value in [0 , 0. , False ]:
976+ as_dict [key ] = value
977+ # other falsy -> str, but empty string is allowed
978+ elif value == '' and context_key in constants .EMPTY_STRING_CONTEXTS :
979+ as_dict [key ] = ''
980+
981+ return as_dict
982+
835983 def to_dict (self ):
836984 """Generate a :class:`dict <python:dict>` representation of the object compatible
837985 with the Highcharts JavaScript library.
@@ -877,7 +1025,16 @@ def to_json(self,
8771025 if filename :
8781026 filename = validators .path (filename )
8791027
880- as_dict = self .to_dict ()
1028+ untrimmed = self ._to_untrimmed_dict ()
1029+
1030+ as_dict = self .trim_dict (untrimmed ,
1031+ to_json = True ,
1032+ context = self .__class__ .__name__ )
1033+
1034+ for key in as_dict :
1035+ if as_dict [key ] == constants .EnforcedNull or as_dict [key ] is None :
1036+ as_dict [key ] = None
1037+
8811038 try :
8821039 as_json = json .dumps (as_dict , encoding = encoding )
8831040 except TypeError :
0 commit comments