Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Release 1.3.0
=========================================

* **BUGFIX:** Fixed incorrect serialization of datetime and Pandas ``Timestamp`` objects in ``.to_dict()`` and ``.to_json()`` (#74).
* **BUGFIX:** Fixed incorrect serialization of ``EnforcedNull`` in ``.to_dict()`` and ``.to_json()`` (#75).
* **ENHANCEMENT:** Added ``__repr__()`` method for Highcharts Core for Python classes (#76).
* **ENHANCEMENT:** Added ``__str__()`` method with special handling for difficult-to-read classes (#76).
* **ENHANCEMENT:** Added ``Chart.get_script_tags()`` to retrieve Javascript ``<script>`` tags (#78).
Expand Down
27 changes: 22 additions & 5 deletions highcharts_core/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,10 @@ def trim_dict(untrimmed: dict,
as_dict[key] = trimmed_value
# Enforced null
elif isinstance(value, constants.EnforcedNullType):
as_dict[key] = 'null'
if to_json:
as_dict[key] = None
else:
as_dict[key] = value
# dict -> object
elif isinstance(value, dict):
trimmed_value = HighchartsMeta.trim_dict(value,
Expand All @@ -288,9 +291,23 @@ def trim_dict(untrimmed: dict,
context = context)
if trimmed_value:
as_dict[key] = trimmed_value
# Pandas Timestamp
elif checkers.is_type(value, 'Timestamp'):
as_dict[key] = value.timestamp()
# 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,
Expand Down Expand Up @@ -440,7 +457,7 @@ def to_json(self,
context = self.__class__.__name__)

for key in as_dict:
if as_dict[key] == constants.EnforcedNull or as_dict[key] == 'null':
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)
Expand Down
87 changes: 85 additions & 2 deletions tests/test_metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
from json.decoder import JSONDecodeError
from validator_collection import checkers

try:
import orjson as json
json_as_bytes = True
except ImportError:
json_as_bytes = False
try:
import rapidjson as json
except ImportError:
try:
import simplejson as json
except ImportError:
import json


class TestClass(HighchartsMeta):
"""Class used to test the :class:`HighchartsMeta` functionality."""
Expand Down Expand Up @@ -396,7 +409,10 @@ def _get_kwargs_from_dict(cls, as_dict):
if not error:
obj = ClassWithTimestamp()
result = obj.to_json()
assert 'timestamp_value' in result
if json_as_bytes:
assert b'timestamp_value' in result
else:
assert 'timestamp_value' in result
as_dict = json.loads(result)
assert 'timestamp_value' in as_dict
assert checkers.is_numeric(as_dict['timestamp_value']) is True
Expand All @@ -405,11 +421,78 @@ def _get_kwargs_from_dict(cls, as_dict):
obj = ClassWithTimestamp()


@pytest.mark.parametrize('error', [
(None),
])
def test_to_json_with_date(error):
from datetime import datetime, date
import json

class ClassWithDate(HighchartsMeta):
@property
def date_value(self):
return date.today()

def _to_untrimmed_dict(self, in_cls=None) -> dict:
return {
'date_value': self.date_value
}

@classmethod
def _get_kwargs_from_dict(cls, as_dict):
return {}

if not error:
obj = ClassWithDate()
result = obj.to_json()
if json_as_bytes:
assert b'date_value' in result
else:
assert 'date_value' in result
as_dict = json.loads(result)
assert 'date_value' in as_dict
assert checkers.is_string(as_dict['date_value']) is True
else:
with pytest.raises(error):
obj = ClassWithDate()


@pytest.mark.parametrize('instance, expected', [
(test_class_camel_case_instance, "TestClassCamelCase(item1 = 123, item2 = 456, camel_case_item = 'test')"),
(constants.EnforcedNull, "EnforcedNullType()"),
(test_class_camel_case_iterable, "TestClassCamelCase(item1 = [1, 2, 'null'], item2 = 456, camel_case_item = 'test')"),
])
def test__repr__(instance, expected):
result = repr(instance)
assert result == expected
assert result == expected


class ClassWithEnforcedNull(HighchartsMeta):
@property
def enforced_null_value(self):
return constants.EnforcedNull

def _to_untrimmed_dict(self, in_cls=None) -> dict:
return {
'enforced_null_value': self.enforced_null_value
}

@classmethod
def _get_kwargs_from_dict(cls, as_dict):
return {}


def test_enforced_null_to_dict():
obj = ClassWithEnforcedNull()
result = obj.to_dict()
assert 'enforced_null_value' in result
assert isinstance(result['enforced_null_value'], constants.EnforcedNullType) is True


def test_enforced_null_to_json():
obj = ClassWithEnforcedNull()
result = obj.to_json()
if json_as_bytes:
assert result == b'{"enforced_null_value":null}'
else:
assert result == '{"enforced_null_value": null}'