From 8899211c6221b858543e351764b4920df72bc3a3 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 3 Nov 2023 13:04:20 -0400 Subject: [PATCH 01/35] Fixed bug in JS literal serialization that accidentally serialized strings as dicts. Closes #130 --- highcharts_core/js_literal_functions.py | 26 ++++++++++++++++++------- tests/fixtures.py | 1 + tests/options/test_tooltips.py | 19 +++++++++++++++++- tests/test_js_literal_functions.py | 18 +++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/highcharts_core/js_literal_functions.py b/highcharts_core/js_literal_functions.py index 628592e4..0ff27d48 100644 --- a/highcharts_core/js_literal_functions.py +++ b/highcharts_core/js_literal_functions.py @@ -159,15 +159,25 @@ def is_js_object(as_str, careful_validation = False): is_empty = as_str[1:-1].strip() == '' if is_empty: return True - has_colon = ':' in as_str - if has_colon: - return True - if 'new ' in as_str: + colon_count = as_str.count(':') + open_brace_count = as_str.count('{') + close_brace_count = as_str.count('}') + brace_set_count = (open_brace_count + close_brace_count) / 2 + if colon_count > 0: + if brace_set_count == 1: + return True + elif brace_set_count > 1 and colon_count >= brace_set_count: + return True + else: + careful_validation = True + elif 'new ' in as_str: return True - if 'Object.create(' in as_str: + elif 'Object.create(' in as_str: return True - return False - else: + else: + return False + + if careful_validation: expression_item = f'const testName = {as_str}' try: parsed = esprima.parseScript(expression_item) @@ -192,6 +202,8 @@ def is_js_object(as_str, careful_validation = False): return True return False + + return False def attempt_variable_declaration(as_str): diff --git a/tests/fixtures.py b/tests/fixtures.py index 3dfdc265..8c87f01e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -796,6 +796,7 @@ def Class_from_js_literal(cls, input_files, filename, as_file, error): assert isinstance(result, cls) is True as_js_literal = result.to_js_literal() + print(as_js_literal) #print('-----------------') #print('RESULT VALIDATION') if 'pattern:' in as_js_literal: diff --git a/tests/options/test_tooltips.py b/tests/options/test_tooltips.py index f983258b..95a35819 100644 --- a/tests/options/test_tooltips.py +++ b/tests/options/test_tooltips.py @@ -9,7 +9,7 @@ from highcharts_core import errors from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ - Class_from_js_literal + Class_from_js_literal, compare_js_literals STANDARD_PARAMS = [ ({}, None), @@ -86,6 +86,9 @@ }""", 'split': True }, None), + ({ + 'format': '{point.name} {point.y}' + }, None), ({ 'border_width': 'not-a-number' @@ -128,3 +131,17 @@ def test_to_dict(kwargs, error): ]) def test_from_js_literal(input_files, filename, as_file, error): Class_from_js_literal(cls, input_files, filename, as_file, error) + + +def test_bug130_tooltip_serialization(): + as_js_literal = """{ + format: '{point.name}: {point.y}' + }""" + + obj = cls.from_js_literal(as_js_literal) + assert obj is not None + assert isinstance(obj, cls) is True + assert obj.format == '{point.name}: {point.y}' + + result = obj.to_js_literal() + assert "'{point.name}: {point.y}'" in result or '"{point.name}: {point.y}"' in result \ No newline at end of file diff --git a/tests/test_js_literal_functions.py b/tests/test_js_literal_functions.py index 00b52c79..7d39ca9f 100644 --- a/tests/test_js_literal_functions.py +++ b/tests/test_js_literal_functions.py @@ -190,3 +190,21 @@ def test_convert_js_property_to_python(original_str, override, expected, error): else: with pytest.raises(error): result = js.convert_js_property_to_python(item, original_str) + + +@pytest.mark.parametrize('original_str, expected, error', [ + ("Object.create({item1:true})", True, None), + ("new Date()", True, None), + ("test string", False, None), + ("{item1: 456}", True, None), + ("'{point.name}: {point.y}'", False, None), + ("{item: {subitem: 123}}", True, None), + ("{item: 123, item2: 456, item3: {subitem: 789}}", True, None), +]) +def test_is_js_object(original_str, expected, error): + if not error: + result = js.is_js_object(original_str) + assert result is expected + else: + with pytest.raises(error): + result = js.is_js_object(original_str) \ No newline at end of file From a9b2806e3d1e55a51e5a6c148edfbe53c720fbad Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 3 Nov 2023 13:06:11 -0400 Subject: [PATCH 02/35] Bumped version number and updated changelog. --- CHANGES.rst | 9 +++++++++ highcharts_core/__version__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 138570db..761b3d1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,13 @@ +Release 1.5.1 +========================================= + +* **BUGFIX:** Fixed bug in JS literal serialization that would misinterpret strings that + start with ``{``, end with ``}``, and contain a colon (``:``) as an object literal rather + than as a string. (#130) + +-------------------- + Release 1.5.0 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index 618528c7..1b37c690 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.5.0' \ No newline at end of file +__version__ = '1.5.1' \ No newline at end of file From f68dfb2d087e7aedb9644e2817a5045bc62b0fe9 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 2 Nov 2023 13:30:55 +0100 Subject: [PATCH 03/35] optional eventlistener --- highcharts_core/chart.py | 260 ++++++++++++++++++++------------------- 1 file changed, 133 insertions(+), 127 deletions(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 26099432..01791b2f 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -20,42 +20,42 @@ class Chart(HighchartsMeta): def __init__(self, **kwargs): """Creates a :class:`Chart ` instance. - + When creating a :class:`Chart ` instance, you can provide any of the object's properties as keyword arguments. **Positional arguments are not supported**. - - In addition to the standard properties, there are three special keyword - arguments which streamline the creation of + + In addition to the standard properties, there are three special keyword + arguments which streamline the creation of :class:`Chart ` instances: - - * ``series`` which accepts an iterable of + + * ``series`` which accepts an iterable of :class:`SeriesBase ` descendents. These are automatically then populated as series within the chart. - + .. note:: - + Each member of ``series`` must be coercable into a Highcharts Core for Python series. And it must contain a ``type`` property. - + * ``data`` which accepts an iterable of objects that are coercable to Highcharts - data point objects, which are then automatically used to create/populate a + data point objects, which are then automatically used to create/populate a series on your chart instance * ``series_type`` which accepts a string indicating the type of series to render for your data. - + .. warning:: - - If you supply ``series``, the ``data`` and ``series_type`` keywords will be + + If you supply ``series``, the ``data`` and ``series_type`` keywords will be *ignored*. - - If you supply ``data``, then ``series_type`` must *also* be supplied. Failure - to do so will raise a + + If you supply ``data``, then ``series_type`` must *also* be supplied. Failure + to do so will raise a :exc:`HighchartsValueError `. - If you are also supplying an - :meth:`options ` keyword argument, then - any series derived from ``series``, ``data``, and ``series_type`` will be + If you are also supplying an + :meth:`options ` keyword argument, then + any series derived from ``series``, ``data``, and ``series_type`` will be *added* to any series defined in that ``options`` value. :raises: :exc:`HighchartsValueError ` @@ -106,19 +106,19 @@ def __str__(self): """Return a human-readable :class:`str ` representation of the chart. .. warning:: - - To ensure that the result is human-readable, the chart's ``options`` property will - be rendered *without* its ``plot_options`` and ``series`` sub-properties. - + + To ensure that the result is human-readable, the chart's ``options`` property will + be rendered *without* its ``plot_options`` and ``series`` sub-properties. + .. tip:: - - If you would like a *complete* and *unambiguous* :class:`str ` + + If you would like a *complete* and *unambiguous* :class:`str ` representation, then you can: - + * use the :meth:`__repr__() ` method, * call ``repr(my_chart)``, or * serialize the chart to JSON using ``my_chart.to_json()``. - + :returns: A :class:`str ` representation of the chart. :rtype: :class:`str ` """ @@ -140,7 +140,7 @@ def __str__(self): kwargs_as_str += f'options = {kwargs[key]}' else: kwargs_as_str += f'{key} = {repr(kwargs[key])}' - + return f'{self.__class__.__name__}({kwargs_as_str})' def _jupyter_include_scripts(self, **kwargs): @@ -148,13 +148,13 @@ def _jupyter_include_scripts(self, **kwargs): :rtype: :class:`str ` """ - required_modules = [f'{self.module_url}{x}' + required_modules = [f'{self.module_url}{x}' for x in self.get_required_modules(include_extension = True)] js_str = '' for item in required_modules: js_str += utility_functions.jupyter_add_script(item) js_str += """.then(() => {""" - + for item in required_modules: js_str += """});""" @@ -264,32 +264,32 @@ def _repr_html_(self): def get_script_tags(self, as_str = False) -> List[str] | str: """Return the collection of ``' + scripts = [f'' for x in self.get_required_modules(include_extension = True)] if as_str: return '\n'.join(scripts) - + return scripts - def get_required_modules(self, + 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 + + :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 ` of :class:`str ` """ initial_scripts = ['highcharts'] @@ -298,12 +298,12 @@ def get_required_modules(self, return scripts def _get_jupyter_script_loader(self, chart_js_str) -> str: - """Return the JavaScript code that loads ``required_modules`` in a Jupyter + """Return the JavaScript code that loads ``required_modules`` in a Jupyter context. - + :param chart_js_str: The JavaScript code that renders the chart. :type chart_js_str: :class:`str ` - + :returns: The JavaScript code that loads the required modules. :rtype: :class:`str ` """ @@ -315,7 +315,7 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: if_requirejs += """ paths: {\n""" if_requirejs += f""" 'highcharts': '{self.module_url}'\n""" if_requirejs += """ }\n\n});""" - + if_requirejs += """ require([""" requirejs_modules = [] for item in self.get_required_modules(): @@ -325,7 +325,7 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: revised_item = f'highcharts/{item}' if revised_item not in requirejs_modules: requirejs_modules.append(revised_item) - + for index, item in enumerate(requirejs_modules): is_last = index == len(requirejs_modules) - 1 if_requirejs += f"""'{item}'""" @@ -335,7 +335,7 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: if_requirejs += chart_js_str if_requirejs += """\n});""" - required_modules = [f'{self.module_url}{x}' + required_modules = [f'{self.module_url}{x}' for x in self.get_required_modules(include_extension = True)] for item in required_modules: if_no_requirejs += utility_functions.jupyter_add_script(item) @@ -343,9 +343,9 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: for item in required_modules: if_no_requirejs += """});""" - + js_str = utility_functions.wrap_for_requirejs(if_requirejs, if_no_requirejs) - + return js_str @property @@ -369,46 +369,46 @@ def callback(self, value): @property def module_url(self) -> str: - """The URL from which Highcharts modules should be downloaded when + """The URL from which Highcharts modules should be downloaded when generating the ``""" + js_str = f"""""" + + as_str = f"""{html_str}\n{include_str}\n{js_str}""" + + return as_str def get_script_tags(self, as_str = False) -> List[str] | str: """Return the collection of ``