Skip to content

Commit

Permalink
Merge pull request #85 from highcharts-for-python/develop
Browse files Browse the repository at this point in the history
PR for v.1.3.2
  • Loading branch information
hcpchris authored Aug 10, 2023
2 parents 7105b57 + 2595ea9 commit 76d95e5
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 3 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@

Release 1.3.2
=========================================

* **BUGFIX:** Fixed incorrect handling when defining a new ``Exporting.buttons`` context button under a different key name than ``contextButton``. (#84).

---------------------

Release 1.3.1
=========================================

Expand Down
2 changes: 1 addition & 1 deletion highcharts_core/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.3.1'
__version__ = '1.3.2'
159 changes: 158 additions & 1 deletion highcharts_core/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,154 @@ def from_json(cls, as_json):
def _to_untrimmed_dict(self, in_cls = None) -> dict:
return self.data

@staticmethod
def trim_iterable(untrimmed,
to_json = False,
context: str = None):
"""Convert any :class:`EnforcedNullType` values in ``untrimmed`` to ``'null'``.
:param untrimmed: The iterable whose members may still be
:obj:`None <python:None>` or Python objects.
:type untrimmed: iterable
:param to_json: If ``True``, will remove all members from ``untrimmed`` that are
not serializable to JSON. Defaults to ``False``.
:type to_json: :class:`bool <python:bool>`
:param context: If provided, will inform the method of the context in which it is
being run which may inform special handling cases (e.g. where empty strings may
be important / allowable). Defaults to :obj:`None <python:None>`.
:type context: :class:`str <python:str>` or :obj:`None <python:None>`
:rtype: iterable
"""
if not checkers.is_iterable(untrimmed, forbid_literals = (str, bytes, dict)):
return untrimmed

trimmed = []
for item in untrimmed:
if checkers.is_type(item, 'CallbackFunction') and to_json:
continue
elif item is None or item == constants.EnforcedNull:
trimmed.append('null')
elif hasattr(item, 'trim_dict'):
updated_context = item.__class__.__name__
untrimmed_item = item._to_untrimmed_dict()
item_as_dict = HighchartsMeta.trim_dict(untrimmed_item,
to_json = to_json,
context = updated_context)
if item_as_dict:
trimmed.append(item_as_dict)
elif isinstance(item, dict):
if item:
trimmed.append(HighchartsMeta.trim_dict(item,
to_json = to_json,
context = context))
elif checkers.is_iterable(item, forbid_literals = (str, bytes, dict)):
if item:
trimmed.append(HighchartsMeta.trim_iterable(item,
to_json = to_json,
context = context))
else:
trimmed.append(item)

return trimmed

@staticmethod
def trim_dict(untrimmed: dict,
to_json: bool = False,
context: str = None) -> dict:
"""Remove keys from ``untrimmed`` whose values are :obj:`None <python:None>` and
convert values that have ``.to_dict()`` methods.
:param untrimmed: The :class:`dict <python:dict>` whose values may still be
:obj:`None <python:None>` or Python objects.
:type untrimmed: :class:`dict <python:dict>`
:param to_json: If ``True``, will remove all keys from ``untrimmed`` that are not
serializable to JSON. Defaults to ``False``.
:type to_json: :class:`bool <python:bool>`
:param context: If provided, will inform the method of the context in which it is
being run which may inform special handling cases (e.g. where empty strings may
be important / allowable). Defaults to :obj:`None <python:None>`.
:type context: :class:`str <python:str>` or :obj:`None <python:None>`
:returns: Trimmed :class:`dict <python:dict>`
:rtype: :class:`dict <python:dict>`
"""
as_dict = {}
for key in untrimmed:
context_key = f'{context}.{key}'
value = untrimmed.get(key, None)
# bool -> Boolean
if isinstance(value, bool):
as_dict[key] = value
# Callback Function
elif checkers.is_type(value, 'CallbackFunction') and to_json:
continue
# HighchartsMeta -> dict --> object
elif value and hasattr(value, '_to_untrimmed_dict'):
untrimmed_value = value._to_untrimmed_dict()
updated_context = value.__class__.__name__
trimmed_value = HighchartsMeta.trim_dict(untrimmed_value,
to_json = to_json,
context = updated_context)
if trimmed_value:
as_dict[key] = trimmed_value
# Enforced null
elif isinstance(value, constants.EnforcedNullType):
if to_json:
as_dict[key] = None
else:
as_dict[key] = value
# dict -> object
elif isinstance(value, dict):
trimmed_value = HighchartsMeta.trim_dict(value,
to_json = to_json,
context = context)
if trimmed_value:
as_dict[key] = trimmed_value
# iterable -> array
elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict)):
trimmed_value = HighchartsMeta.trim_iterable(value,
to_json = to_json,
context = context)
if trimmed_value:
as_dict[key] = trimmed_value
# Datetime or Datetime-like
elif checkers.is_datetime(value):
trimmed_value = value
if to_json:
if not value.tzinfo:
trimmed_value = value.replace(tzinfo = datetime.timezone.utc)
as_dict[key] = trimmed_value.timestamp() * 1000
elif hasattr(trimmed_value, 'to_pydatetime'):
as_dict[key] = trimmed_value.to_pydatetime()
else:
as_dict[key] = trimmed_value
# Date or Time
elif checkers.is_date(value) or checkers.is_time(value):
if to_json:
as_dict[key] = value.isoformat()
else:
as_dict[key] = value
# other truthy -> str / number
elif value:
trimmed_value = HighchartsMeta.trim_iterable(value,
to_json = to_json,
context = context)
if trimmed_value:
as_dict[key] = trimmed_value
# other falsy -> str / number
elif value in [0, 0., False]:
as_dict[key] = value
# other falsy -> str, but empty string is allowed
elif value == '' and context_key in constants.EMPTY_STRING_CONTEXTS:
as_dict[key] = ''

return as_dict

def to_dict(self):
"""Generate a :class:`dict <python:dict>` representation of the object compatible
with the Highcharts JavaScript library.
Expand Down Expand Up @@ -877,7 +1025,16 @@ def to_json(self,
if filename:
filename = validators.path(filename)

as_dict = self.to_dict()
untrimmed = self._to_untrimmed_dict()

as_dict = self.trim_dict(untrimmed,
to_json = True,
context = self.__class__.__name__)

for key in as_dict:
if as_dict[key] == constants.EnforcedNull or as_dict[key] is None:
as_dict[key] = None

try:
as_json = json.dumps(as_dict, encoding = encoding)
except TypeError:
Expand Down
2 changes: 1 addition & 1 deletion highcharts_core/utility_classes/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ class ExportingButtons(JavaScriptDict):
:class:`ButtonConfiguration`.
"""
_valid_value_types = ButtonConfiguration
_valid_value_types = ContextButtonConfiguration
_allow_empty_value = True

def __init__(self, **kwargs):
Expand Down
19 changes: 19 additions & 0 deletions tests/utility_classes/test_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,22 @@ def test_ExportingButtons_from_dict(kwargs, error):
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3)
def test_ExportingButtons_to_dict(kwargs, error):
Class_to_dict(cls3, kwargs, error)


def test_issue84_ExportingButtons_as_ContextButtonConfiguration():
as_dict = {
'contextButton': {
'enabled': False
},
'exportButton': {
'text': "Download",
'menuItems': ['downloadPNG']
}
}
instance = cls3.from_dict(as_dict)
for key in as_dict:
print({f'Instance: {instance.to_json()}'})
print({f'Instance: {instance.to_js_literal()}'})
assert key in instance
assert does_kwarg_value_match_result(as_dict[key],
instance.get(key)) is True

0 comments on commit 76d95e5

Please sign in to comment.