diff --git a/CHANGES.rst b/CHANGES.rst index 3f3c99ec..450ff187 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,16 @@ +Release 1.3.0 +========================================= + +* **ENHANCEMENT:** Modified the way that data points are serialized to JavaScript literal objects. Now, they are serialized to a JavaScript array if their configured properties are those that Highcharts (JS) supports in JavaScript array notation. Otherwise, the code falls back to serialize the data point as a JavaScript object literal. This change is intended to improve performance and reduce the size of the serialized data. (#77) +* **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 ``' + for x in self.get_required_modules(include_extension = True)] + + if as_str: + return '\n'.join(scripts) + + return scripts + + def get_required_modules(self, + include_extension = False) -> List[str]: """Return the list of URLs from which the Highcharts JavaScript modules needed to render the chart can be retrieved. :param include_extension: if ``True``, will return script names with the ``'.js'`` extension included. Defaults to ``False``. :type include_extension: :class:`bool ` - - :rtype: :class:`list ` + + :rtype: :class:`list ` of :class:`str ` """ initial_scripts = ['highcharts'] scripts = self._process_required_modules(initial_scripts, include_extension) @@ -189,8 +254,9 @@ def callback(self, value): @property def module_url(self) -> str: """The URL from which Highcharts modules should be downloaded when - generating the ``', + '', + '' + ], None), + ("""{ + "chart": { + "type": "column" + }, + "colors": null, + "credits": false, + "exporting": { + "scale": 1 + }, + "series": [{ + "baseSeries": 1, + "color": "#434343", + "name": "Pareto", + "tooltip": { + "valueDecimals": 2, + "valueSuffix": "%" + }, + "type": "pareto", + "yAxis": 1, + "zIndex": 10 + }, { + "color": "#7cb5ec", + "data": [1, 23, 45, 54, 84, 13, 8, 7, 23, 1, 34, 6, 8, 99, 85, 23, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1], + "name": "random-name", + "type": "column", + "zIndex": 2 + }], + "title": { + "text": "Random Name Pareto" + }, + "tooltip": { + "shared": true + }, + "xAxis": { + "categories": ["Something", "Something", "Something", "Something", "Something", "Something", "Hypovolemia", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something", "Something"], + "crosshair": true, + "labels": { + "rotation": 90 + } + }, + "yAxis": [{ + "title": { + "text": "count" + } + }, { + "labels": { + "format": "{value}%" + }, + "max": 100, + "maxPadding": 0, + "min": 0, + "minPadding": 0, + "opposite": true, + "title": { + "text": "accum percent" + } + }] + }""", + True, + """\n\n""", None), +]) +def test_get_script_tags(options_str, as_str, expected, error): + from highcharts_core.options import HighchartsOptions + options = HighchartsOptions.from_json(options_str) + chart = cls.from_options(options) + + if not error: + result = chart.get_script_tags(as_str = as_str) + if isinstance(expected, list): + assert isinstance(result, list) is True + assert len(result) == len(expected) + for item in expected: + assert item in result + elif result: + assert result == expected + else: + assert result is None or len(result) == 0 + else: + with pytest.raises(error): + result = chart.get_script_tags(as_str = as_str) + + +@pytest.mark.parametrize('kwargs, error', [ + ({}, None), + ({ + 'container': 'my-container-name', + 'module_url': 'https://mycustomurl.com/', + 'options': { + 'title': { + 'text': 'My Chart' + } + } + }, None), +]) +def test__repr__(kwargs, error): + obj = cls(**kwargs) + if not error: + result = repr(obj) + if 'options' in kwargs: + assert 'options = ' in result + else: + with pytest.raises(error): + result = repr(obj) + + +@pytest.mark.parametrize('kwargs, error', [ + ({}, None), + ({ + 'container': 'my-container-name', + 'module_url': 'https://mycustomurl.com/', + 'options': { + 'title': { + 'text': 'My Chart' + } + } + }, None), +]) +def test__str__(kwargs, error): + obj = cls(**kwargs) + if not error: + result = str(obj) + print(result) + if 'options' in kwargs: + assert 'options = ' in result + else: + with pytest.raises(error): + result = str(obj) \ No newline at end of file diff --git a/tests/test_metaclasses.py b/tests/test_metaclasses.py index 0dd09726..e8198d8c 100644 --- a/tests/test_metaclasses.py +++ b/tests/test_metaclasses.py @@ -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.""" @@ -36,9 +49,40 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: test_class_instance = TestClass(item1 = 123, item2 = 456) test_class_trimmed_instance = TestClass(item1 = 123) -test_class_iterable = TestClass(item1 = [1, 2, constants.EnforcedNullType], item2 = 456) +test_class_iterable = TestClass(item1 = [1, 2, constants.EnforcedNull], item2 = 456) test_class_none_iterable = TestClass(item1 = [1, None, 3], item2 = 456) + +class TestClassCamelCase(TestClass): + def __init__(self, **kwargs): + self.camel_case_item = kwargs.get('camel_case_item', None) + + super().__init__(**kwargs) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'item1': as_dict.get('item1', None), + 'item2': as_dict.get('item2', None), + 'camel_case_item': as_dict.get('camelCaseItem', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + return { + 'item1': self.item1, + 'item2': self.item2, + 'camelCaseItem': self.camel_case_item + } + +test_class_camel_case_instance = TestClassCamelCase(item1 = 123, item2 = 456, camel_case_item = 'test') +test_class_camel_case_trimmed_instance = TestClassCamelCase(item1 = 123, camel_case_item = 'test') +test_class_camel_case_iterable = TestClassCamelCase(item1 = [1, 2, constants.EnforcedNull], item2 = 456, camel_case_item = 'test') +test_class_camel_case__none_iterable = TestClassCamelCase(item1 = [1, None, 3], item2 = 456, camel_case_item = 'test') + + + @pytest.mark.parametrize('kwargs, error', [ ({'item1': 123, 'item2': 456}, @@ -333,3 +377,122 @@ def test_from_js_literal(cls, as_str, error): else: with pytest.raises(error): result = cls.from_js_literal(as_str) + +@pytest.mark.parametrize('error', [ + (None), +]) +def test_to_json_with_timestamp(error): + from datetime import datetime + import json + + try: + from pandas import Timestamp + import_successful = True + except ImportError: + import_successful = False + + if import_successful: + class ClassWithTimestamp(HighchartsMeta): + @property + def timestamp_value(self): + return Timestamp(datetime.utcnow()) + + def _to_untrimmed_dict(self, in_cls=None) -> dict: + return { + 'timestamp_value': self.timestamp_value + } + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + return {} + + if not error: + obj = ClassWithTimestamp() + result = obj.to_json() + 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 + else: + with pytest.raises(error): + 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 + + +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}' \ No newline at end of file diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index e7f47d76..8c6cdc50 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -22,4 +22,18 @@ def test_parse_csv(kwargs, expected_column_names, expected_records, error): assert len(records_as_dicts) == expected_records else: with pytest.raises(error): - result = utility_functions.parse_csv(**kwargs) \ No newline at end of file + result = utility_functions.parse_csv(**kwargs) + + +@pytest.mark.parametrize('camelCase, expected, error', [ + ('camelCase', 'camel_case', None), + ('camelCaseURL', 'camel_case_url', None), + ('camel123Case', 'camel123_case', None), +]) +def test_to_snake_case(camelCase, expected, error): + if not error: + result = utility_functions.to_snake_case(camelCase) + assert result == expected + else: + with pytest.raises(error): + result = utility_functions.to_snake_case(camelCase)