From 4d475790c70b91341bb0c51b8066af1f28d9c672 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Thu, 14 Sep 2023 12:02:29 -0400 Subject: [PATCH 1/2] Implemented careful_validation flag on serialization to JS literals. --- CHANGES.rst | 2 + highcharts_core/chart.py | 29 +- .../global_options/shared_options.py | 18 +- highcharts_core/js_literal_functions.py | 256 +++++++++++++----- highcharts_core/metaclasses.py | 45 ++- highcharts_core/options/series/data/base.py | 27 +- .../options/series/data/collections.py | 23 +- .../utility_classes/javascript_functions.py | 6 +- 8 files changed, 317 insertions(+), 89 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 27ef0bda..51b6dfa7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,8 @@ Release 1.4.0 ========================================= +* **MAJOR** performance gains in the ``.to_js_literal()`` method. Implementation seems to + improve performance by 50 - 90%. (#51) * **ENHANCEMENT:** Added one-shot chart creation and rendering from Series objects (#89). * **ENHANCEMENT:** Added one-shot chart creation using ``series`` and ``data``/``series_type`` keywords. (#90). diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 213f39d9..bedc3170 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -456,7 +456,8 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -468,6 +469,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + .. note:: If :meth:`variable_name ` is set, will render a string as @@ -487,7 +500,9 @@ def to_js_literal(self, as_dict = {} for key in untrimmed: item = untrimmed[key] - serialized = serialize_to_js_literal(item, encoding = encoding) + serialized = serialize_to_js_literal(item, + encoding = encoding, + careful_validation = careful_validation) if serialized is not None: as_dict[key] = serialized @@ -499,13 +514,19 @@ def to_js_literal(self, container_as_str = """null""" if self.options: - options_as_str = "{}".format(self.options.to_js_literal(encoding = encoding)) + options_as_str = "{}".format( + self.options.to_js_literal(encoding = encoding, + careful_validation = careful_validation) + ) else: options_as_str = """null""" callback_as_str = '' if self.callback: - callback_as_str = "{}".format(self.callback.to_js_literal(encoding = encoding)) + callback_as_str = "{}".format( + self.callback.to_js_literal(encoding = encoding, + careful_validation = careful_validation) + ) signature_elements += 1 signature = """Highcharts.chart(""" diff --git a/highcharts_core/global_options/shared_options.py b/highcharts_core/global_options/shared_options.py index 8e246c6e..82370276 100644 --- a/highcharts_core/global_options/shared_options.py +++ b/highcharts_core/global_options/shared_options.py @@ -16,7 +16,8 @@ class SharedOptions(HighchartsOptions): def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -28,6 +29,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + .. note:: Returns a JavaScript string which applies the Highcharts global options. The @@ -40,7 +53,8 @@ def to_js_literal(self, :rtype: :class:`str ` """ prefix = 'Highcharts.setOptions(' - options_body = super().to_js_literal(encoding = encoding) + options_body = super().to_js_literal(encoding = encoding, + careful_validation = careful_validation) as_str = prefix + options_body + ');' diff --git a/highcharts_core/js_literal_functions.py b/highcharts_core/js_literal_functions.py index bf17b747..0c9feadd 100644 --- a/highcharts_core/js_literal_functions.py +++ b/highcharts_core/js_literal_functions.py @@ -17,7 +17,10 @@ from highcharts_core import constants, errors, utility_functions -def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False) -> Optional[str]: +def serialize_to_js_literal(item, + encoding = 'utf-8', + ignore_to_array = False, + careful_validation = False) -> Optional[str]: """Convert ``item`` to the contents of a JavaScript object literal code snippet. :param item: A value that is to be converted into a JS object literal notation value. @@ -30,6 +33,18 @@ def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False) - to break recursion. Defaults to ``False``. :type ignore_to_array: :class:`bool ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :returns: A JavaScript object literal code snippet, expressed as a string. Or :obj:`None ` if ``item`` is not serializable. :rtype: :class:`str ` or :obj:`None ` @@ -37,33 +52,41 @@ def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False) - if not ignore_to_array and hasattr(item, 'to_array'): requires_js_objects = getattr(item, 'requires_js_object', True) if requires_js_objects and hasattr(item, 'to_js_literal'): - return item.to_js_literal(encoding = encoding) + return item.to_js_literal(encoding = encoding, + careful_validation = careful_validation) elif requires_js_objects: return serialize_to_js_literal(item, encoding = encoding, - ignore_to_array = True) + ignore_to_array = True, + careful_validation = careful_validation) else: - return serialize_to_js_literal(item.to_array(), encoding = encoding) + return serialize_to_js_literal(item.to_array(), + encoding = encoding, + careful_validation = careful_validation) elif HAS_NUMPY and isinstance(item, np.ndarray): return utility_functions.from_ndarray(item) - elif checkers.is_iterable(item, forbid_literals = (str, bytes, dict, UserDict)): + elif not isinstance(item, (str, bytes, dict, UserDict)) and hasattr(item, '__iter__'): requires_js_objects = any([getattr(x, 'requires_js_object', True) for x in item]) if requires_js_objects: return [serialize_to_js_literal(x, encoding = encoding, - ignore_to_array = True) + ignore_to_array = True, + careful_validation = careful_validation) for x in item] else: - return [serialize_to_js_literal(x.to_array(), encoding = encoding) + return [serialize_to_js_literal(x.to_array(), + encoding = encoding, + careful_validation = careful_validation) for x in item] elif hasattr(item, 'to_js_literal'): - return item.to_js_literal(encoding = encoding) + return item.to_js_literal(encoding = encoding, + careful_validation = careful_validation) elif isinstance(item, constants.EnforcedNullType) or item == 'null': return constants.EnforcedNull elif isinstance(item, bool): return item - elif checkers.is_string(item): + elif isinstance(item, str): return_value = item.replace("'", "\\'") return return_value elif checkers.is_numeric(item) and not isinstance(item, Decimal): @@ -72,15 +95,17 @@ def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False) - return float(item) elif checkers.is_type(item, ('CallbackFunction')): return str(item) - elif checkers.is_type(item, (dict, UserDict)): + elif isinstance(item, (dict, UserDict)): as_dict = {} for key in item: - as_dict[key] = serialize_to_js_literal(item[key], encoding = encoding) + as_dict[key] = serialize_to_js_literal(item[key], + encoding = encoding, + careful_validation = careful_validation) return str(as_dict) elif checkers.is_datetime(item): if not item.tzinfo: item = item.replace(tzinfo = datetime.timezone.utc) - return item.timestamp()*1000 + return item.timestamp() * 1000 elif checkers.is_date(item): return f'Date.UTC({item.year}, {item.month - 1}, {item.day})' elif checkers.is_time(item): @@ -91,39 +116,65 @@ def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False) - return None -def is_js_object(as_str): +def is_js_object(as_str, careful_validation = False): """Determine whether ``as_str`` is a JavaScript object. :param as_str: The string to evaluate. :type as_str: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :returns: ``True`` if ``as_str`` is a JavaScript function. ``False`` if not. :rtype: :class:`bool ` """ - expression_item = f'const testName = {as_str}' - try: - parsed = esprima.parseScript(expression_item) - except ParseError: + + if not careful_validation: + 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: + return True + if 'Object.create(' in as_str: + return True + return False + else: + expression_item = f'const testName = {as_str}' try: - parsed = esprima.parseModule(expression_item) + parsed = esprima.parseScript(expression_item) except ParseError: + try: + parsed = esprima.parseModule(expression_item) + except ParseError: + return False + + body = parsed.body + if not body: return False - body = parsed.body - if not body: - return False + first_item = body[0] + if first_item.type != 'VariableDeclaration': + return False - first_item = body[0] - if first_item.type != 'VariableDeclaration': - return False + init = first_item.declarations[0].init + if not init: + return False + if init.type in ('ObjectExpression'): + return True - init = first_item.declarations[0].init - if not init: return False - if init.type in ('ObjectExpression'): - return True - - return False def attempt_variable_declaration(as_str): @@ -167,63 +218,118 @@ def attempt_variable_declaration(as_str): return False -def is_js_function_or_class(as_str) -> bool: +def is_js_function_or_class(as_str, careful_validation = False) -> bool: """Determine whether ``as_str`` is a JavaScript function or not. :param as_str: The string to evaluate. :type as_str: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :returns: ``True`` if ``as_str`` is a JavaScript function. ``False`` if not. :rtype: :class:`bool ` """ - if not checkers.is_string(as_str): + if not isinstance(as_str, str): return False + if not careful_validation: + is_function = as_str.startswith('function ') or as_str.startswith('function*') + if is_function: + return True - try: - parsed = esprima.parseScript(as_str) - except ParseError: - try: - parsed = esprima.parseModule(as_str) - except ParseError: - if as_str.startswith('function') is False: - return False - else: - return attempt_variable_declaration(as_str) + is_function = 'function(' in as_str or 'function*(' in as_str + if is_function: + return True - body = parsed.body - if not body: - return False + is_function = ')=>' in as_str or ') =>' in as_str - first_item = body[0] - if first_item.type in ('FunctionDeclaration', 'ClassDeclaration'): - return True - elif as_str.startswith('function') or as_str.startswith('class'): - return attempt_variable_declaration(as_str) - elif first_item.type == 'VariableDeclaration': - init = first_item.declarations[0].init - if not init: + if is_function: + return True + + is_function = 'new Function(' in as_str + if is_function: + return True + + is_class = as_str.startswith('class ') + if is_class: + return True + + is_class = 'class {' in as_str or 'class{' in as_str + if is_class: + return True + + is_class = '= class' in as_str or '=class' in as_str + if is_class: + return True + + return False + else: + try: + parsed = esprima.parseScript(as_str) + except ParseError: + try: + parsed = esprima.parseModule(as_str) + except ParseError: + if as_str.startswith('function') is False: + return False + else: + return attempt_variable_declaration(as_str) + + body = parsed.body + if not body: return False - if init.type in ('FunctionExpression', 'ArrowFunctionExpression', - 'ClassExpression'): + + first_item = body[0] + if first_item.type in ('FunctionDeclaration', 'ClassDeclaration'): return True - elif init.type == 'NewExpression': - callee = init.callee - if not callee: + elif as_str.startswith('function') or as_str.startswith('class'): + return attempt_variable_declaration(as_str) + elif first_item.type == 'VariableDeclaration': + init = first_item.declarations[0].init + if not init: return False - if callee.name == 'Function': + if init.type in ('FunctionExpression', 'ArrowFunctionExpression', + 'ClassExpression'): return True + elif init.type == 'NewExpression': + callee = init.callee + if not callee: + return False + if callee.name == 'Function': + return True return False -def get_js_literal(item) -> str: +def get_js_literal(item, careful_validation = False) -> str: """Convert the value of ``item`` into a JavaScript literal string. + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :returns: The JavaScript literal string. :rtype: :class:`str ` """ as_str = '' - if checkers.is_iterable(item, forbid_literals = (str, bytes, dict, UserDict)): + if not isinstance(item, (str, bytes, dict, UserDict)) and hasattr(item, '__iter__'): subitems = [get_js_literal(x) for x in item] as_str += '[' subitem_counter = 0 @@ -235,11 +341,11 @@ def get_js_literal(item) -> str: if subitem_counter < len(subitems): as_str += ',\n' as_str += ']' - elif checkers.is_string(item): + elif isinstance(item, str): if (item.startswith('[') or item.startswith('Date')) and item != 'Date': as_str += f"""{item}""" elif item.startswith('{') and item.endswith('}'): - if is_js_object(item): + if is_js_object(item, careful_validation = careful_validation): as_str += f"""{item}""" elif "'" in item: item = item.replace("'", "\\'") @@ -251,7 +357,7 @@ def get_js_literal(item) -> str: elif item.startswith == 'HCP: REPLACE-WITH-': item_str = item.replace('HCP: REPLACE-WITH-', '') as_str += f"""{item_str}""" - elif not is_js_function_or_class(item): + elif not is_js_function_or_class(item, careful_validation = careful_validation): as_str += f"""'{item}'""" else: as_str += f"""{item}""" @@ -267,7 +373,9 @@ def get_js_literal(item) -> str: return as_str -def assemble_js_literal(as_dict, keys_as_strings = False) -> Optional[str]: +def assemble_js_literal(as_dict, + keys_as_strings = False, + careful_validation = False) -> Optional[str]: """Convert ``as_dict`` into a JavaScript object literal string. :param as_dict: A :class:`dict ` representation of the JavaScript object. @@ -278,10 +386,24 @@ def assemble_js_literal(as_dict, keys_as_strings = False) -> Optional[str]: to ``False``. :type keys_as_strings: :class:`bool ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :returns: The JavaScript object literal representation of ``as_dict``. :rtype: :class:`str ` or :obj:`None ` """ - as_dict = validators.dict(as_dict, allow_empty = True) + if careful_validation: + as_dict = validators.dict(as_dict, allow_empty = True) + if not as_dict: return None @@ -303,7 +425,7 @@ def assemble_js_literal(as_dict, keys_as_strings = False) -> Optional[str]: else: as_str += f""" {key}: """ - as_str += get_js_literal(item) + as_str += get_js_literal(item, careful_validation = careful_validation) if current_key < keys: as_str += ',\n' diff --git a/highcharts_core/metaclasses.py b/highcharts_core/metaclasses.py index 35c3251f..9f2cf4cb 100644 --- a/highcharts_core/metaclasses.py +++ b/highcharts_core/metaclasses.py @@ -498,7 +498,8 @@ def to_json(self, def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -510,6 +511,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :rtype: :class:`str ` or :obj:`None ` """ if filename: @@ -519,11 +532,14 @@ def to_js_literal(self, as_dict = {} for key in untrimmed: item = untrimmed[key] - serialized = serialize_to_js_literal(item, encoding = encoding) + serialized = serialize_to_js_literal(item, + encoding = encoding, + careful_validation = careful_validation) if serialized is not None: as_dict[key] = serialized - as_str = assemble_js_literal(as_dict) + as_str = assemble_js_literal(as_dict, + careful_validation = careful_validation) if filename: with open(filename, 'w', encoding = encoding) as file_: @@ -1080,7 +1096,8 @@ def to_json(self, def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -1092,6 +1109,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :rtype: :class:`str ` or :obj:`None ` """ if filename: @@ -1101,11 +1130,15 @@ def to_js_literal(self, as_dict = {} for key in untrimmed: item = untrimmed[key] - serialized = serialize_to_js_literal(item, encoding = encoding) + serialized = serialize_to_js_literal(item, + encoding = encoding, + careful_validation = careful_validation) if serialized is not None: as_dict[key] = serialized - as_str = assemble_js_literal(as_dict, keys_as_strings = True) + as_str = assemble_js_literal(as_dict, + keys_as_strings = True, + careful_validation = careful_validation) if filename: with open(filename, 'w', encoding = encoding) as file_: diff --git a/highcharts_core/options/series/data/base.py b/highcharts_core/options/series/data/base.py index ab4f4cfa..675cea14 100644 --- a/highcharts_core/options/series/data/base.py +++ b/highcharts_core/options/series/data/base.py @@ -705,7 +705,8 @@ def from_ndarray(cls, value): def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -717,6 +718,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :rtype: :class:`str ` or :obj:`None ` """ if filename: @@ -727,15 +740,19 @@ def to_js_literal(self, as_dict = {} for key in untrimmed: item = untrimmed[key] - serialized = serialize_to_js_literal(item, encoding = encoding) + serialized = serialize_to_js_literal(item, + encoding = encoding, + careful_validation = careful_validation) if serialized is not None: as_dict[key] = serialized - as_str = assemble_js_literal(as_dict) + as_str = assemble_js_literal(as_dict, + careful_validation = careful_validation) else: - serialized = serialize_to_js_literal(untrimmed) + serialized = serialize_to_js_literal(untrimmed, + careful_validation = careful_validation) if isinstance(serialized, list): - as_str = ','.join([get_js_literal(x) + as_str = ','.join([get_js_literal(x, careful_validation = careful_validation) for x in serialized]) as_str = f'[{as_str}]' else: diff --git a/highcharts_core/options/series/data/collections.py b/highcharts_core/options/series/data/collections.py index f0fcce71..380d4104 100644 --- a/highcharts_core/options/series/data/collections.py +++ b/highcharts_core/options/series/data/collections.py @@ -590,7 +590,8 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: def to_js_literal(self, filename = None, - encoding = 'utf-8') -> Optional[str]: + encoding = 'utf-8', + careful_validation = False) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -602,6 +603,18 @@ def to_js_literal(self, to ``'utf-8'``. :type encoding: :class:`str ` + :param careful_validation: if ``True``, will carefully validate JavaScript values + along the way using the + `esprima-python `__ library. Defaults + to ``False``. + + .. warning:: + + Setting this value to ``True`` will significantly degrade serialization + performance, though it may prove useful for debugging purposes. + + :type careful_validation: :class:`bool ` + :rtype: :class:`str ` or :obj:`None ` """ if filename: @@ -611,10 +624,14 @@ def to_js_literal(self, is_ndarray = all([isinstance(x, list) for x in untrimmed]) if not is_ndarray: as_str = '[' - as_str += ','.join([x.to_js_literal() for x in untrimmed]) + as_str += ','.join([x.to_js_literal(encoding = encoding, + careful_validation = careful_validation) + for x in untrimmed]) as_str += ']' else: - serialized = serialize_to_js_literal(untrimmed) + serialized = serialize_to_js_literal(untrimmed, + encoding = encoding, + careful_validation = careful_validation) as_str = serialized if filename: diff --git a/highcharts_core/utility_classes/javascript_functions.py b/highcharts_core/utility_classes/javascript_functions.py index b3262b40..6b5f7249 100644 --- a/highcharts_core/utility_classes/javascript_functions.py +++ b/highcharts_core/utility_classes/javascript_functions.py @@ -132,7 +132,8 @@ def to_json(self, encoding = 'utf-8'): def to_js_literal(self, filename = None, - encoding = 'utf-8') -> str: + encoding = 'utf-8', + careful_validation = False) -> str: if filename: filename = validators.path(filename) @@ -484,7 +485,8 @@ def from_js_literal(cls, def to_js_literal(self, filename = None, - encoding = 'utf-8') -> str: + encoding = 'utf-8', + careful_validation = False) -> str: if filename: filename = validators.path(filename) From 52e3795212b6ac3dff6e78205f37475706718efd Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Thu, 14 Sep 2023 12:03:20 -0400 Subject: [PATCH 2/2] Optimized some validation steps needed for the .to_json() methods. --- CHANGES.rst | 2 ++ highcharts_core/metaclasses.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 51b6dfa7..641b0e0a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Release 1.4.0 * **MAJOR** performance gains in the ``.to_js_literal()`` method. Implementation seems to improve performance by 50 - 90%. (#51) +* *SIGNIFICANT* performance gains in the ``.to_json()`` method. Implementation seems to + improve performance by 30 - 90%. * **ENHANCEMENT:** Added one-shot chart creation and rendering from Series objects (#89). * **ENHANCEMENT:** Added one-shot chart creation using ``series`` and ``data``/``series_type`` keywords. (#90). diff --git a/highcharts_core/metaclasses.py b/highcharts_core/metaclasses.py index 9f2cf4cb..710a2a7c 100644 --- a/highcharts_core/metaclasses.py +++ b/highcharts_core/metaclasses.py @@ -206,7 +206,9 @@ def trim_iterable(untrimmed, if HAS_NUMPY and isinstance(untrimmed, np.ndarray): return untrimmed - if not checkers.is_iterable(untrimmed, forbid_literals = (str, bytes, dict)): + if isinstance(untrimmed, + (str, bytes, dict, UserDict)) or not hasattr(untrimmed, + '__iter__'): return untrimmed trimmed = [] @@ -228,7 +230,9 @@ def trim_iterable(untrimmed, trimmed.append(HighchartsMeta.trim_dict(item, to_json = to_json, context = context)) - elif checkers.is_iterable(item, forbid_literals = (str, bytes, dict)): + elif not isinstance(item, + (str, bytes, dict, UserDict)) and hasattr(item, + '__iter__'): if item: trimmed.append(HighchartsMeta.trim_iterable(item, to_json = to_json, @@ -306,7 +310,9 @@ def trim_dict(untrimmed: dict, if trimmed_value: as_dict[key] = trimmed_value # iterable -> array - elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict)): + elif not isinstance(value, + (str, bytes, dict, UserDict)) and hasattr(value, + '__iter__'): trimmed_value = HighchartsMeta.trim_iterable(value, to_json = to_json, context = context)