Skip to content

Commit 76d95e5

Browse files
authored
Merge pull request #85 from highcharts-for-python/develop
PR for v.1.3.2
2 parents 7105b57 + 2595ea9 commit 76d95e5

File tree

5 files changed

+186
-3
lines changed

5 files changed

+186
-3
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11

2+
Release 1.3.2
3+
=========================================
4+
5+
* **BUGFIX:** Fixed incorrect handling when defining a new ``Exporting.buttons`` context button under a different key name than ``contextButton``. (#84).
6+
7+
---------------------
8+
29
Release 1.3.1
310
=========================================
411

highcharts_core/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.3.1'
1+
__version__ = '1.3.2'

highcharts_core/metaclasses.py

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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:

highcharts_core/utility_classes/buttons.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class ExportingButtons(JavaScriptDict):
586586
:class:`ButtonConfiguration`.
587587
588588
"""
589-
_valid_value_types = ButtonConfiguration
589+
_valid_value_types = ContextButtonConfiguration
590590
_allow_empty_value = True
591591

592592
def __init__(self, **kwargs):

tests/utility_classes/test_buttons.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,22 @@ def test_ExportingButtons_from_dict(kwargs, error):
223223
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3)
224224
def test_ExportingButtons_to_dict(kwargs, error):
225225
Class_to_dict(cls3, kwargs, error)
226+
227+
228+
def test_issue84_ExportingButtons_as_ContextButtonConfiguration():
229+
as_dict = {
230+
'contextButton': {
231+
'enabled': False
232+
},
233+
'exportButton': {
234+
'text': "Download",
235+
'menuItems': ['downloadPNG']
236+
}
237+
}
238+
instance = cls3.from_dict(as_dict)
239+
for key in as_dict:
240+
print({f'Instance: {instance.to_json()}'})
241+
print({f'Instance: {instance.to_js_literal()}'})
242+
assert key in instance
243+
assert does_kwarg_value_match_result(as_dict[key],
244+
instance.get(key)) is True

0 commit comments

Comments
 (0)