From ad16a6110deb0f534d69f350d90ea7b08e2b9c04 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Wed, 2 Aug 2023 08:49:47 -0400 Subject: [PATCH 01/17] Added unit test for Pandas Timestamp serialization. --- highcharts_core/metaclasses.py | 2 +- tests/test_metaclasses.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/highcharts_core/metaclasses.py b/highcharts_core/metaclasses.py index a2187fdb..1389bcf2 100644 --- a/highcharts_core/metaclasses.py +++ b/highcharts_core/metaclasses.py @@ -275,7 +275,7 @@ def trim_dict(untrimmed: dict, as_dict[key] = value.timestamp() # other truthy -> str / number elif value: - trimmed_value = HighchartsMeta.trim_iterable(value, + trimmed_value = HighchartsMeta.trim_iterable(value, to_json = to_json, context = context) if trimmed_value: diff --git a/tests/test_metaclasses.py b/tests/test_metaclasses.py index 0dd09726..1316cb86 100644 --- a/tests/test_metaclasses.py +++ b/tests/test_metaclasses.py @@ -333,3 +333,42 @@ 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() + 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() From 03c6872150c16dfee68457ffe06e5a875dd1b55a Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Wed, 2 Aug 2023 20:19:27 -0400 Subject: [PATCH 02/17] Added __repr__ method. --- highcharts_core/constants.py | 3 +++ highcharts_core/metaclasses.py | 3 +++ tests/test_metaclasses.py | 12 +++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/highcharts_core/constants.py b/highcharts_core/constants.py index 37c0e525..0e340a4c 100644 --- a/highcharts_core/constants.py +++ b/highcharts_core/constants.py @@ -28,6 +28,9 @@ class EnforcedNullType: def __eq__(self, other): return isinstance(other, self.__class__) + + def __repr__(self): + return "EnforcedNullType()" EnforcedNull = EnforcedNullType() diff --git a/highcharts_core/metaclasses.py b/highcharts_core/metaclasses.py index 1389bcf2..f0c91649 100644 --- a/highcharts_core/metaclasses.py +++ b/highcharts_core/metaclasses.py @@ -41,6 +41,9 @@ def __eq__(self, other): return self_js_literal == other_js_literal + def __repr__(self): + return f'{self.__class__.__name__}.from_dict({self.to_dict()})' + @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. diff --git a/tests/test_metaclasses.py b/tests/test_metaclasses.py index 1316cb86..7ed378cd 100644 --- a/tests/test_metaclasses.py +++ b/tests/test_metaclasses.py @@ -36,7 +36,7 @@ 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) @pytest.mark.parametrize('kwargs, error', [ @@ -372,3 +372,13 @@ def _get_kwargs_from_dict(cls, as_dict): else: with pytest.raises(error): obj = ClassWithTimestamp() + + +@pytest.mark.parametrize('instance, expected', [ + (test_class_instance, "TestClass.from_dict({'item1': 123, 'item2': 456})"), + (constants.EnforcedNull, "EnforcedNullType()"), + (test_class_iterable, "TestClass.from_dict({'item1': [1, 2, 'null'], 'item2': 456})"), +]) +def test__repr(instance, expected): + result = repr(instance) + assert result == expected \ No newline at end of file From 3a844615ee4463d6524653025c19ad1110a656f2 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Wed, 2 Aug 2023 20:19:46 -0400 Subject: [PATCH 03/17] Bumped version number. --- CHANGES.rst | 7 +++++++ highcharts_core/__version__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3f3c99ec..64951b75 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +Release 1.2.7 +========================================= + +* **ENHANCEMENT:** Custom ``__repr__()`` method for Highcharts Core for Python classes. + +------------------ + Release 1.2.6 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index 3fd380a7..d50b1386 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.2.6' \ No newline at end of file +__version__ = '1.2.7' \ No newline at end of file From f5b3d4cc45baa87793eb8dcfd924f4301c27d939 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 4 Aug 2023 11:52:16 -0400 Subject: [PATCH 04/17] Implemented Chart.get_script_tags(). Closes #78. --- highcharts_core/chart.py | 37 +++++++-- highcharts_core/metaclasses.py | 7 +- tests/test_chart.py | 147 +++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 9 deletions(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 632e24f8..6e809de2 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -1,3 +1,4 @@ +import os from typing import Optional, List from collections import UserDict @@ -30,7 +31,9 @@ def __init__(self, **kwargs): self.container = kwargs.get('container', None) self.options = kwargs.get('options', None) self.variable_name = kwargs.get('variable_name', None) - self.module_url = kwargs.get('module_url', 'https://code.highcharts.com/') + self.module_url = kwargs.get('module_url', + None) or os.environ.get('HIGHCHARTS_MODULE_URL', + 'https://code.highcharts.com/') super().__init__(**kwargs) @@ -152,15 +155,36 @@ def _repr_html_(self): """ return self.display() - def get_required_modules(self, include_extension = False) -> List[str]: + def get_script_tags(self, as_str = False) -> List[str] | str: + """Return the collection of ``' + 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 +213,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) \ No newline at end of file From e6a59a24d14500b10bbef29929f2ef65839af211 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 4 Aug 2023 11:52:44 -0400 Subject: [PATCH 05/17] Bumped version to 1.3.0 and updated changelog. --- CHANGES.rst | 5 +++-- highcharts_core/__version__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 64951b75..764c03a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,8 @@ -Release 1.2.7 +Release 1.3.0 ========================================= -* **ENHANCEMENT:** Custom ``__repr__()`` method for Highcharts Core for Python classes. +* **ENHANCEMENT:** Added a complete ``__repr__()`` method for Highcharts Core for Python classes. +* **ENHANCEMENT:** Added ``Chart.get_script_tags()`` to retrieve Javascript ``