diff --git a/CHANGES.rst b/CHANGES.rst
index 967ba4c..c21d700 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,6 +3,7 @@ Release 1.10.3
=========================================
* **BUGFIX:** Fixed support for missing ``HoverState`` options. Closes #211.
+* **BUGFIX:** Fixed JavaScript serialization of ``CallbackFunction``. Closes #213.
----
diff --git a/highcharts_core/utility_classes/javascript_functions.py b/highcharts_core/utility_classes/javascript_functions.py
index cfff0ad..696bb5c 100644
--- a/highcharts_core/utility_classes/javascript_functions.py
+++ b/highcharts_core/utility_classes/javascript_functions.py
@@ -17,31 +17,31 @@ def __init__(self, **kwargs):
self._arguments = None
self._body = None
- self.function_name = kwargs.get('function_name', None)
- self.arguments = kwargs.get('arguments', None)
- self.body = kwargs.get('body', None)
+ self.function_name = kwargs.get("function_name", None)
+ self.arguments = kwargs.get("arguments", None)
+ self.body = kwargs.get("body", None)
def __str__(self) -> str:
if self.function_name:
- prefix = f'function {self.function_name}'
+ prefix = f"function {self.function_name}"
else:
- prefix = 'function'
+ prefix = "function"
- arguments = '('
+ arguments = "("
if self.arguments:
for argument in self.arguments:
- arguments += f'{argument},'
+ arguments += f"{argument},"
arguments = arguments[:-1]
- arguments += ')'
+ arguments += ")"
- as_str = f'{prefix}{arguments}'
- as_str += ' {'
+ as_str = f"{prefix}{arguments}"
+ as_str += " {"
if self.body:
- as_str += '\n'
+ as_str += "\n"
as_str += self.body
- as_str += '}'
+ as_str += "}"
return as_str
@@ -61,7 +61,7 @@ def function_name(self) -> Optional[str]:
@function_name.setter
def function_name(self, value):
- self._function_name = validators.variable_name(value, allow_empty = True)
+ self._function_name = validators.variable_name(value, allow_empty=True)
@property
def arguments(self) -> Optional[List[str]]:
@@ -80,13 +80,13 @@ def arguments(self, value):
arguments = validators.iterable(value)
validated_value = []
for argument in arguments:
- if '=' not in argument:
+ if "=" not in argument:
validated_value.append(validators.variable_name(argument))
else:
- variable = argument.split('=')[0]
- default_value = argument.split('=')[1]
+ variable = argument.split("=")[0]
+ default_value = argument.split("=")[1]
variable = validators.variable_name(variable)
- validated_value.append(f'{variable}={default_value}')
+ validated_value.append(f"{variable}={default_value}")
self._arguments = validated_value
@@ -117,43 +117,47 @@ def body(self) -> Optional[str]:
@body.setter
def body(self, value):
- self._body = validators.string(value, allow_empty = True)
+ self._body = validators.string(value, allow_empty=True)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
- 'function_name': as_dict.get('function_name',
- as_dict.get('functionName', None)),
- 'arguments': as_dict.get('arguments', None),
- 'body': as_dict.get('body', None)
+ "function_name": as_dict.get(
+ "function_name", as_dict.get("functionName", None)
+ ),
+ "arguments": as_dict.get("arguments", None),
+ "body": as_dict.get("body", None),
}
return kwargs
- def _to_untrimmed_dict(self, in_cls = None) -> dict:
+ def _to_untrimmed_dict(self, in_cls=None) -> dict:
return {
- 'function_name': self.function_name,
- 'arguments': self.arguments,
- 'body': self.body
+ "function_name": self.function_name,
+ "arguments": self.arguments,
+ "body": self.body,
}
- def to_json(self, encoding = 'utf-8', for_export: bool = False):
+ def to_json(self, encoding="utf-8", for_export: bool = False):
if for_export:
- return str(self)
+ as_str = str(self)
+ if '"' in as_str:
+ as_str = as_str.replace('"', '\\"')
+
+ return as_str
return None
- def to_js_literal(self,
- filename = None,
- encoding = 'utf-8',
- careful_validation = False) -> str:
+ def to_js_literal(
+ self, filename=None, encoding="utf-8", careful_validation=False
+ ) -> str:
if filename:
filename = validators.path(filename)
as_str = str(self)
if filename:
- with open(filename, 'w', encoding = encoding) as file_:
+ with open(filename, "w", encoding=encoding) as file_:
file_.write(as_str)
return as_str
@@ -175,17 +179,24 @@ def _convert_from_js_ast(cls, property_definition, original_str):
:returns: :class:`CallbackFunction`
"""
- if not checkers.is_type(property_definition, ('FunctionDeclaration',
- 'FunctionExpression',
- 'MethodDefinition',
- 'Property')):
- raise errors.HighchartsParseError(f'property_definition should contain a '
- f'FunctionExpression, FunctionDeclaration, '
- 'MethodDefinition, or Property instance. '
- f'Received: '
- f'{property_definition.__class__.__name__}')
-
- if property_definition.type not in ['MethodDefinition', 'Property']:
+ if not checkers.is_type(
+ property_definition,
+ (
+ "FunctionDeclaration",
+ "FunctionExpression",
+ "MethodDefinition",
+ "Property",
+ ),
+ ):
+ raise errors.HighchartsParseError(
+ f"property_definition should contain a "
+ f"FunctionExpression, FunctionDeclaration, "
+ "MethodDefinition, or Property instance. "
+ f"Received: "
+ f"{property_definition.__class__.__name__}"
+ )
+
+ if property_definition.type not in ["MethodDefinition", "Property"]:
body = property_definition.body
else:
body = property_definition.value.body
@@ -194,12 +205,14 @@ def _convert_from_js_ast(cls, property_definition, original_str):
body_start = body_range[0] + 1
body_end = body_range[1] - 1
- if property_definition.type == 'FunctionDeclaration':
+ if property_definition.type == "FunctionDeclaration":
function_name = property_definition.id.name
- elif property_definition.type == 'MethodDefinition':
+ elif property_definition.type == "MethodDefinition":
function_name = property_definition.key.name
- elif property_definition.type == 'FunctionExpression' and \
- property_definition.id is not None:
+ elif (
+ property_definition.type == "FunctionExpression"
+ and property_definition.id is not None
+ ):
function_name = property_definition.id.name
else:
function_name = None
@@ -207,28 +220,28 @@ def _convert_from_js_ast(cls, property_definition, original_str):
function_body = original_str[body_start:body_end]
arguments = []
- if property_definition.type in ['MethodDefinition', 'Property']:
+ if property_definition.type in ["MethodDefinition", "Property"]:
for item in property_definition.value.params:
if item.name:
arguments.append(item.name)
elif item.left.name and item.right.name:
- arguments.append(f'{item.left.name}={item.right.name}')
+ arguments.append(f"{item.left.name}={item.right.name}")
else:
for item in property_definition.params:
if item.name:
arguments.append(item.name)
elif item.left.name and item.right.name:
- arguments.append(f'{item.left.name}={item.right.name}')
+ arguments.append(f"{item.left.name}={item.right.name}")
- return cls(function_name = function_name,
- arguments = arguments,
- body = function_body)
+ return cls(function_name=function_name, arguments=arguments, body=function_body)
@classmethod
- def from_js_literal(cls,
- as_str_or_file,
- allow_snake_case: bool = True,
- _break_loop_on_failure: bool = False):
+ def from_js_literal(
+ cls,
+ as_str_or_file,
+ allow_snake_case: bool = True,
+ _break_loop_on_failure: bool = False,
+ ):
"""Return a Python object representation of a Highcharts JavaScript object
literal.
@@ -251,33 +264,29 @@ def from_js_literal(cls,
"""
is_file = checkers.is_file(as_str_or_file)
if is_file:
- with open(as_str_or_file, 'r') as file_:
+ with open(as_str_or_file, "r") as file_:
as_str = file_.read()
else:
as_str = as_str_or_file
parsed, updated_str = cls._validate_js_function(as_str)
- if parsed.body[0].type == 'FunctionDeclaration':
+ if parsed.body[0].type == "FunctionDeclaration":
property_definition = parsed.body[0]
- elif parsed.body[0].type == 'MethodDefinition':
+ elif parsed.body[0].type == "MethodDefinition":
property_definition = parsed.body[0].body[0]
- elif parsed.body[0].type != 'FunctionDeclaration':
+ elif parsed.body[0].type != "FunctionDeclaration":
property_definition = parsed.body[0].declarations[0].init
return cls._convert_from_js_ast(property_definition, updated_str)
@classmethod
- def from_python(cls,
- callable,
- model = 'gpt-3.5-turbo',
- api_key = None,
- **kwargs):
+ def from_python(cls, callable, model="gpt-3.5-turbo", api_key=None, **kwargs):
"""Return a :class:`CallbackFunction` having converted a Python callable into
a JavaScript function using the generative AI ``model`` indicated.
-
+
.. note::
-
- Because this relies on the outside APIs exposed by
+
+ Because this relies on the outside APIs exposed by
`OpenAI `__ and `Anthropic `__,
if you wish to use one of their models you *must* supply your own API key.
These are paid services which they provide, and so you *will* be incurring
@@ -285,10 +294,10 @@ def from_python(cls,
:param callable: The Python callable to convert.
:type callable: callable
-
- :param model: The generative AI model to use.
+
+ :param model: The generative AI model to use.
Defaults to ``'gpt-3.5-turbo'``. Accepts:
-
+
* ``'gpt-3.5-turbo'`` (default)
* ``'gpt-3.5-turbo-16k'``
* ``'gpt-4'``
@@ -303,9 +312,9 @@ def from_python(cls,
:obj:`None `, which then tries to find the API
key in the appropriate environment variable:
- * ``OPENAI_API_KEY`` if using an
+ * ``OPENAI_API_KEY`` if using an
`OpenAI `__ provided model
- * ``ANTHROPIC_API_KEY`` if using an
+ * ``ANTHROPIC_API_KEY`` if using an
`Anthropic `__ provided model
:type api_key: :class:`str ` or :obj:`None `
@@ -316,15 +325,15 @@ def from_python(cls,
:returns: The ``CallbackFunction`` representation of the JavaScript
code that does the same as the ``callable`` argument.
-
+
.. warning::
Generating the JavaScript source code is *not* deterministic.
- That means that it may not be correct, and we **STRONGLY**
- recommend reviewing it before using it in a production
+ That means that it may not be correct, and we **STRONGLY**
+ recommend reviewing it before using it in a production
application.
- Every single generative AI is known to have issues - whether
+ Every single generative AI is known to have issues - whether
"hallucinations", biases, or incoherence. We cannot stress
enough:
@@ -339,7 +348,7 @@ def from_python(cls,
:raises HighchartsValueError: if no ``api_key`` is available
:raises HighchartsDependencyError: if a required dependency is not
available in the runtime environment
- :raises HighchartsModerationError: if using an OpenAI model, and
+ :raises HighchartsModerationError: if using an OpenAI model, and
OpenAI detects that the supplied input violates their usage policies
:raises HighchartsPythonConversionError: if the model was unable to
convert ``callable`` into JavaScript source code
@@ -352,17 +361,14 @@ def from_python(cls,
except errors.HighchartsParseError:
raise errors.HighchartsPythonConversionError(
f'The JavaScript function generated by model "{model}" '
- f'failed to be validated as a proper JavaScript function. '
- f'Please retry, or select a different model and retry.'
+ f"failed to be validated as a proper JavaScript function. "
+ f"Please retry, or select a different model and retry."
)
-
+
return obj
@classmethod
- def _validate_js_function(cls,
- as_str,
- range = True,
- _break_loop_on_failure = False):
+ def _validate_js_function(cls, as_str, range=True, _break_loop_on_failure=False):
"""Parse a JavaScript function from within ``as_str``.
:param as_str: A string that potentially contains a JavaScript function.
@@ -380,26 +386,28 @@ def _validate_js_function(cls,
:class:`str `
"""
try:
- parsed = esprima.parseScript(as_str, loc = range, range = range)
+ parsed = esprima.parseScript(as_str, loc=range, range=range)
except ParseError:
try:
- parsed = esprima.parseModule(as_str, loc = range, range = range)
+ parsed = esprima.parseModule(as_str, loc=range, range=range)
except ParseError:
- if not _break_loop_on_failure and as_str.startswith('function'):
+ if not _break_loop_on_failure and as_str.startswith("function"):
as_str = f"""const testFunction = {as_str}"""
- return cls._validate_js_function(as_str,
- range = range,
- _break_loop_on_failure = True)
+ return cls._validate_js_function(
+ as_str, range=range, _break_loop_on_failure=True
+ )
elif not _break_loop_on_failure:
as_str = f"""const testFunction = function {as_str}"""
- return cls._validate_js_function(as_str,
- range = range,
- _break_loop_on_failure = True)
+ return cls._validate_js_function(
+ as_str, range=range, _break_loop_on_failure=True
+ )
else:
- raise errors.HighchartsParseError('._validate_js_function() expects '
- 'a str containing a valid '
- 'JavaScript function. Could not '
- 'find a valid function.')
+ raise errors.HighchartsParseError(
+ "._validate_js_function() expects "
+ "a str containing a valid "
+ "JavaScript function. Could not "
+ "find a valid function."
+ )
return parsed, as_str
@@ -411,32 +419,34 @@ def __init__(self, **kwargs):
self._class_name = None
self._methods = None
- self.class_name = kwargs.get('class_name', None)
- self.methods = kwargs.get('methods', None)
+ self.class_name = kwargs.get("class_name", None)
+ self.methods = kwargs.get("methods", None)
def __str__(self) -> str:
if not self.class_name:
- raise errors.HighchartsMissingClassNameError('Unable to serialize. The '
- 'JavaScriptClass instance has '
- 'no class_name provided.')
- as_str = f'class {self.class_name} '
- as_str += '{\n'
+ raise errors.HighchartsMissingClassNameError(
+ "Unable to serialize. The "
+ "JavaScriptClass instance has "
+ "no class_name provided."
+ )
+ as_str = f"class {self.class_name} "
+ as_str += "{\n"
for method in self.methods or []:
- method_str = f'{method.function_name}'
- argument_str = '('
+ method_str = f"{method.function_name}"
+ argument_str = "("
for argument in method.arguments or []:
- argument_str += f'{argument},'
+ argument_str += f"{argument},"
if method.arguments:
argument_str = argument_str[:-1]
- argument_str += ') {\n'
+ argument_str += ") {\n"
method_str += argument_str
- method_str += method.body + '\n}\n'
+ method_str += method.body + "\n}\n"
as_str += method_str
- as_str += '}'
+ as_str += "}"
return as_str
@@ -450,7 +460,7 @@ def class_name(self) -> Optional[str]:
@class_name.setter
def class_name(self, value):
- self._class_name = validators.variable_name(value, allow_empty = True)
+ self._class_name = validators.variable_name(value, allow_empty=True)
@property
def methods(self) -> Optional[List[CallbackFunction]]:
@@ -488,38 +498,36 @@ def methods(self, value):
if not value:
self._methods = None
else:
- value = validate_types(value,
- types = CallbackFunction,
- force_iterable = True)
+ value = validate_types(value, types=CallbackFunction, force_iterable=True)
has_constructor = False
for method in value:
if not method.function_name:
- raise errors.HighchartsJavaScriptError('All JavaScriptClass methods '
- 'require a function name.')
- if method.function_name == 'constructor':
+ raise errors.HighchartsJavaScriptError(
+ "All JavaScriptClass methods require a function name."
+ )
+ if method.function_name == "constructor":
has_constructor = True
if not has_constructor:
- raise errors.HighchartsJavaScriptError('A JavaScriptClass requires at '
- 'least one "constructor" method. '
- 'Yours had none.')
+ raise errors.HighchartsJavaScriptError(
+ "A JavaScriptClass requires at "
+ 'least one "constructor" method. '
+ "Yours had none."
+ )
self._methods = value
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
- 'class_name': as_dict.get('className', None),
- 'methods': as_dict.get('methods', None)
+ "class_name": as_dict.get("className", None),
+ "methods": as_dict.get("methods", None),
}
return kwargs
- def _to_untrimmed_dict(self, in_cls = None) -> dict:
- return {
- 'className': self.class_name,
- 'methods': self.methods
- }
+ def _to_untrimmed_dict(self, in_cls=None) -> dict:
+ return {"className": self.class_name, "methods": self.methods}
@classmethod
def _convert_from_js_ast(cls, definition, original_str):
@@ -538,11 +546,13 @@ def _convert_from_js_ast(cls, definition, original_str):
:returns: :class:`JavaScriptClass`
"""
- if not checkers.is_type(definition, ('ClassDeclaration', 'ClassExpression')):
- raise errors.HighchartsParseError(f'definition should contain a '
- f'ClassDeclaration or ClassExpression'
- ' instance. Received: '
- f'{definition.__class__.__name__}')
+ if not checkers.is_type(definition, ("ClassDeclaration", "ClassExpression")):
+ raise errors.HighchartsParseError(
+ f"definition should contain a "
+ f"ClassDeclaration or ClassExpression"
+ " instance. Received: "
+ f"{definition.__class__.__name__}"
+ )
class_name = definition.id.name
@@ -556,12 +566,10 @@ def _convert_from_js_ast(cls, definition, original_str):
methods = [CallbackFunction.from_js_literal(x) for x in method_strings]
- return cls(class_name = class_name,
- methods = methods)
+ return cls(class_name=class_name, methods=methods)
@classmethod
- def from_js_literal(cls,
- as_str_or_file):
+ def from_js_literal(cls, as_str_or_file):
"""Return a Python object representation of a JavaScript class.
:param as_str_or_file: The JavaScript object literal, represented either as a
@@ -579,35 +587,35 @@ def from_js_literal(cls,
"""
is_file = checkers.is_file(as_str_or_file)
if is_file:
- with open(as_str_or_file, 'r') as file_:
+ with open(as_str_or_file, "r") as file_:
as_str = file_.read()
else:
as_str = as_str_or_file
try:
- parsed = esprima.parseScript(as_str, range = True)
+ parsed = esprima.parseScript(as_str, range=True)
except ParseError:
try:
- parsed = esprima.parseModule(as_str, range = True)
+ parsed = esprima.parseModule(as_str, range=True)
except ParseError:
- raise errors.HighchartsParseError('unable to find a JavaScript class '
- 'declaration in ``as_str``.')
+ raise errors.HighchartsParseError(
+ "unable to find a JavaScript class declaration in ``as_str``."
+ )
definition = parsed.body[0]
return cls._convert_from_js_ast(definition, as_str)
- def to_js_literal(self,
- filename = None,
- encoding = 'utf-8',
- careful_validation = False) -> str:
+ def to_js_literal(
+ self, filename=None, encoding="utf-8", careful_validation=False
+ ) -> str:
if filename:
filename = validators.path(filename)
as_str = str(self)
if filename:
- with open(filename, 'w', encoding = encoding) as file_:
+ with open(filename, "w", encoding=encoding) as file_:
file_.write(as_str)
return as_str
@@ -620,7 +628,7 @@ class VariableName(HighchartsMeta):
def __init__(self, **kwargs):
self._variable_name = None
- self.variable_name = kwargs.get('variable_name', None)
+ self.variable_name = kwargs.get("variable_name", None)
@property
def variable_name(self) -> Optional[str]:
@@ -633,20 +641,21 @@ def variable_name(self) -> Optional[str]:
@variable_name.setter
def variable_name(self, value):
- self._variable_name = validators.variable_name(value, allow_empty = True)
+ self._variable_name = validators.variable_name(value, allow_empty=True)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
- 'variable_name': as_dict.get('variable_name', as_dict.get('variableName',
- None)),
+ "variable_name": as_dict.get(
+ "variable_name", as_dict.get("variableName", None)
+ ),
}
return kwargs
- def _to_untrimmed_dict(self, in_cls = None) -> dict:
+ def _to_untrimmed_dict(self, in_cls=None) -> dict:
untrimmed = {
- 'variableName': self.variable_name,
+ "variableName": self.variable_name,
}
return untrimmed
diff --git a/tests/utility_classes/test_javascript_functions.py b/tests/utility_classes/test_javascript_functions.py
index d18391b..da5b85b 100644
--- a/tests/utility_classes/test_javascript_functions.py
+++ b/tests/utility_classes/test_javascript_functions.py
@@ -14,7 +14,7 @@
from tests.fixtures import disable_ai
-def validate_js_function(as_str, _break_loop_on_failure = False, range = True):
+def validate_js_function(as_str, _break_loop_on_failure=False, range=True):
"""Parse ``as_str`` as a valid JavaScript function.
:param as_str: A putative JavaScript function definition
@@ -25,16 +25,16 @@ def validate_js_function(as_str, _break_loop_on_failure = False, range = True):
:class:`bool `
"""
try:
- parsed = esprima.parseScript(as_str, loc = range, range = range)
+ parsed = esprima.parseScript(as_str, loc=range, range=range)
except ParseError:
try:
- parsed = esprima.parseModule(as_str, loc = range, range = range)
+ parsed = esprima.parseModule(as_str, loc=range, range=range)
except ParseError as error:
if not _break_loop_on_failure:
as_str = f"""const testFunction = {as_str}"""
- return validate_js_function(as_str,
- _break_loop_on_failure = True,
- range = range)
+ return validate_js_function(
+ as_str, _break_loop_on_failure=True, range=range
+ )
else:
raise error
@@ -42,31 +42,31 @@ def validate_js_function(as_str, _break_loop_on_failure = False, range = True):
def func1():
- return 'A value!'
+ return "A value!"
-def func2(arg1, keyword_arg = 'default'):
+def func2(arg1, keyword_arg="default"):
arg1 += 123
return arg1 in keyword_arg
-@pytest.mark.parametrize('kwargs, error', [
- ({}, None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, None),
- ({
- 'function_name': 123
- }, TypeError),
- ({
- 'arguments': 'not-a-list'
- }, TypeError),
- ({
- 'body': 123
- }, TypeError),
-])
+@pytest.mark.parametrize(
+ "kwargs, error",
+ [
+ ({}, None),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ None,
+ ),
+ ({"function_name": 123}, TypeError),
+ ({"arguments": "not-a-list"}, TypeError),
+ ({"body": 123}, TypeError),
+ ],
+)
def test_CallbackFunction__init__(kwargs, error):
if not error:
result = js_f.CallbackFunction(**kwargs)
@@ -80,23 +80,23 @@ def test_CallbackFunction__init__(kwargs, error):
result = js_f.CallbackFunction(**kwargs)
-@pytest.mark.parametrize('as_dict, error', [
- ({}, None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, None),
- ({
- 'function_name': 123
- }, TypeError),
- ({
- 'arguments': 'not-a-list'
- }, TypeError),
- ({
- 'body': 123
- }, TypeError),
-])
+@pytest.mark.parametrize(
+ "as_dict, error",
+ [
+ ({}, None),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ None,
+ ),
+ ({"function_name": 123}, TypeError),
+ ({"arguments": "not-a-list"}, TypeError),
+ ({"body": 123}, TypeError),
+ ],
+)
def test_CallbackFunction_from_dict(as_dict, error):
if not error:
result = js_f.CallbackFunction.from_dict(as_dict)
@@ -110,14 +110,20 @@ def test_CallbackFunction_from_dict(as_dict, error):
result = js_f.CallbackFunction.from_dict(as_dict)
-@pytest.mark.parametrize('kwargs, error', [
- ({}, None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, None),
-])
+@pytest.mark.parametrize(
+ "kwargs, error",
+ [
+ ({}, None),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ None,
+ ),
+ ],
+)
def test_CallbackFunction__to_untrimmed_dict(kwargs, error):
instance = js_f.CallbackFunction(**kwargs)
if not error:
@@ -132,19 +138,30 @@ def test_CallbackFunction__to_untrimmed_dict(kwargs, error):
result = instance._to_untrimmed_dict()
-@pytest.mark.parametrize('kwargs, expected, error', [
- ({}, """function() {}""", None),
- ({
- 'function_name': None,
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, """function(test1,test2) {\nreturn True;}""", None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, """function testFunction(test1,test2) {\nreturn True;}""", None),
-])
+@pytest.mark.parametrize(
+ "kwargs, expected, error",
+ [
+ ({}, """function() {}""", None),
+ (
+ {
+ "function_name": None,
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ """function(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ """function testFunction(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ ],
+)
def test_CallbackFunction__str__(kwargs, expected, error):
instance = js_f.CallbackFunction(**kwargs)
if not error:
@@ -158,31 +175,53 @@ def test_CallbackFunction__str__(kwargs, expected, error):
result = str(instance)
-@pytest.mark.parametrize('kwargs, filename, expected, error', [
- ({}, None, """function() {}""", None),
- ({
- 'function_name': None,
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, None, """function(test1,test2) {\nreturn True;}""", None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, None, """function testFunction(test1,test2) {\nreturn True;}""", None),
-
- ({}, 'test1.js', """function() {}""", None),
- ({
- 'function_name': None,
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, 'test2.js', """function(test1,test2) {\nreturn True;}""", None),
- ({
- 'function_name': 'testFunction',
- 'arguments': ['test1', 'test2'],
- 'body': """return True;"""
- }, 'test3.js', """function testFunction(test1,test2) {\nreturn True;}""", None),
-])
+@pytest.mark.parametrize(
+ "kwargs, filename, expected, error",
+ [
+ ({}, None, """function() {}""", None),
+ (
+ {
+ "function_name": None,
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ None,
+ """function(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ None,
+ """function testFunction(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ ({}, "test1.js", """function() {}""", None),
+ (
+ {
+ "function_name": None,
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ "test2.js",
+ """function(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ "test3.js",
+ """function testFunction(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ ],
+)
def test_CallbackFunction_to_js_literal(tmp_path, kwargs, filename, expected, error):
instance = js_f.CallbackFunction(**kwargs)
if filename:
@@ -194,7 +233,7 @@ def test_CallbackFunction_to_js_literal(tmp_path, kwargs, filename, expected, er
assert result == expected
if filename:
assert checkers.is_file(filename) is True
- with open(filename, 'r') as file_:
+ with open(filename, "r") as file_:
result_as_str = file_.read()
assert result_as_str == expected
else:
@@ -202,53 +241,61 @@ def test_CallbackFunction_to_js_literal(tmp_path, kwargs, filename, expected, er
result = instance.to_js_literal(filename)
-@pytest.mark.parametrize('original_str, error', [
- ("""function() {}""", None),
- ("""function(test1,test2) {\nreturn True;}""", None),
- ("""function testFunction(test1,test2) {\nreturn True;}""", None),
-])
+@pytest.mark.parametrize(
+ "original_str, error",
+ [
+ ("""function() {}""", None),
+ ("""function(test1,test2) {\nreturn True;}""", None),
+ ("""function testFunction(test1,test2) {\nreturn True;}""", None),
+ ],
+)
def test_CallbackFunction_convert_from_js_ast(original_str, error):
original_parsed, updated_str = validate_js_function(original_str)
- unranged_result = validate_js_function(original_str, range = False)
+ unranged_result = validate_js_function(original_str, range=False)
unranged_parsed = unranged_result[0]
- if original_parsed.body[0].type != 'FunctionDeclaration':
+ if original_parsed.body[0].type != "FunctionDeclaration":
property_definition = original_parsed.body[0].declarations[0].init
else:
property_definition = original_parsed.body[0]
if not error:
- result = js_f.CallbackFunction._convert_from_js_ast(property_definition,
- updated_str)
+ result = js_f.CallbackFunction._convert_from_js_ast(
+ property_definition, updated_str
+ )
assert result is not None
assert isinstance(result, js_f.CallbackFunction) is True
as_str = str(result)
- as_str_parsed, updated_as_str = validate_js_function(as_str, range = False)
+ as_str_parsed, updated_as_str = validate_js_function(as_str, range=False)
assert str(as_str_parsed) == str(unranged_parsed)
else:
with pytest.raises(error):
- result = js_f.CallbackFunction._convert_from_js_ast(property_definition,
- updated_str)
-
-
-@pytest.mark.parametrize('original_str, error', [
- ("""function() {}""", None),
- ("""function(test1,test2) {\nreturn true;}""", None),
- ("""function testFunction(test1,test2) {\nreturn true;}""", None),
-
- (123, TypeError),
- ("""const abc = 123;""", errors.HighchartsParseError),
-])
+ result = js_f.CallbackFunction._convert_from_js_ast(
+ property_definition, updated_str
+ )
+
+
+@pytest.mark.parametrize(
+ "original_str, error",
+ [
+ ("""function() {}""", None),
+ ("""function(test1,test2) {\nreturn true;}""", None),
+ ("""function testFunction(test1,test2) {\nreturn true;}""", None),
+ (123, TypeError),
+ ("""const abc = 123;""", errors.HighchartsParseError),
+ ],
+)
def test_CallbackFunction_from_js_literal(original_str, error):
if not error:
- unranged_result = js_f.CallbackFunction._validate_js_function(original_str,
- range = False)
+ unranged_result = js_f.CallbackFunction._validate_js_function(
+ original_str, range=False
+ )
unranged_parsed = unranged_result[0]
result = js_f.CallbackFunction.from_js_literal(original_str)
assert result is not None
assert isinstance(result, js_f.CallbackFunction) is True
as_str = str(result)
- result_parsed = js_f.CallbackFunction._validate_js_function(as_str, range = False)
+ result_parsed = js_f.CallbackFunction._validate_js_function(as_str, range=False)
as_str_parsed = result_parsed[0]
assert str(as_str_parsed) == str(unranged_parsed)
else:
@@ -256,12 +303,14 @@ def test_CallbackFunction_from_js_literal(original_str, error):
result = js_f.CallbackFunction.from_js_literal(original_str)
-@pytest.mark.parametrize('callable, model, error', [
- (func1, 'gpt-3.5-turbo', None),
- (func2, 'gpt-3.5-turbo', None),
-
- (1, 'gpt-3.5-turbo', ValueError),
-])
+@pytest.mark.parametrize(
+ "callable, model, error",
+ [
+ (func1, "gpt-3.5-turbo", None),
+ (func2, "gpt-3.5-turbo", None),
+ (1, "gpt-3.5-turbo", ValueError),
+ ],
+)
def test_CallbackFunction_from_python(disable_ai, callable, model, error):
if not disable_ai:
if not error:
@@ -273,30 +322,53 @@ def test_CallbackFunction_from_python(disable_ai, callable, model, error):
result = js_f.CallbackFunction.from_python(callable, model)
-@pytest.mark.parametrize('kwargs, error', [
- ({}, None),
- ({'class_name': 'TestClass',
- 'methods': []}, None),
- ({'class_name': 'TestClass',
- 'methods': ["""function constructor() { return true; }"""]}, None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor() { return true; }"""]}, None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor() { return true; }""",
- """testMethod(test1, test2) { return true; }"""]}, None),
-
-
- ({'methods': ["""function wrongName() { return true; }"""]}, errors.HighchartsJavaScriptError),
- ({'methods': ["""function() { return true;}"""]}, errors.HighchartsJavaScriptError),
-])
+@pytest.mark.parametrize(
+ "kwargs, error",
+ [
+ ({}, None),
+ ({"class_name": "TestClass", "methods": []}, None),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""function constructor() { return true; }"""],
+ },
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""constructor() { return true; }"""],
+ },
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": [
+ """constructor() { return true; }""",
+ """testMethod(test1, test2) { return true; }""",
+ ],
+ },
+ None,
+ ),
+ (
+ {"methods": ["""function wrongName() { return true; }"""]},
+ errors.HighchartsJavaScriptError,
+ ),
+ (
+ {"methods": ["""function() { return true;}"""]},
+ errors.HighchartsJavaScriptError,
+ ),
+ ],
+)
def test_JavaScriptClass__init__(kwargs, error):
if not error:
result = js_f.JavaScriptClass(**kwargs)
assert result is not None
assert isinstance(result, js_f.JavaScriptClass)
methods = result.methods or []
- if 'methods' in kwargs:
- method_strings = [x for x in kwargs['methods']]
+ if "methods" in kwargs:
+ method_strings = [x for x in kwargs["methods"]]
for key in kwargs:
for method in methods:
for method_string in method_strings:
@@ -311,33 +383,56 @@ def test_JavaScriptClass__init__(kwargs, error):
result = js_f.JavaScriptClass(**kwargs)
-@pytest.mark.parametrize('as_dict, error', [
- ({}, None),
- ({'className': 'TestClass',
- 'methods': []}, None),
- ({'className': 'TestClass',
- 'methods': ["""function constructor() { return true; }"""]}, None),
- ({'className': 'TestClass',
- 'methods': ["""constructor() { return true; }"""]}, None),
- ({'className': 'TestClass',
- 'methods': ["""constructor() { return true; }""",
- """testMethod(test1, test2) { return true; }"""]}, None),
-
-
- ({'methods': ["""function wrongName() { return true; }"""]}, errors.HighchartsJavaScriptError),
- ({'methods': ["""function() { return true;}"""]}, errors.HighchartsJavaScriptError),
-])
+@pytest.mark.parametrize(
+ "as_dict, error",
+ [
+ ({}, None),
+ ({"className": "TestClass", "methods": []}, None),
+ (
+ {
+ "className": "TestClass",
+ "methods": ["""function constructor() { return true; }"""],
+ },
+ None,
+ ),
+ (
+ {
+ "className": "TestClass",
+ "methods": ["""constructor() { return true; }"""],
+ },
+ None,
+ ),
+ (
+ {
+ "className": "TestClass",
+ "methods": [
+ """constructor() { return true; }""",
+ """testMethod(test1, test2) { return true; }""",
+ ],
+ },
+ None,
+ ),
+ (
+ {"methods": ["""function wrongName() { return true; }"""]},
+ errors.HighchartsJavaScriptError,
+ ),
+ (
+ {"methods": ["""function() { return true;}"""]},
+ errors.HighchartsJavaScriptError,
+ ),
+ ],
+)
def test_JavaScriptClass_from_dict(as_dict, error):
if not error:
result = js_f.JavaScriptClass.from_dict(as_dict)
assert result is not None
assert isinstance(result, js_f.JavaScriptClass) is True
- if as_dict.get('className'):
+ if as_dict.get("className"):
assert result.class_name is not None
- assert result.class_name == as_dict.get('className')
+ assert result.class_name == as_dict.get("className")
methods = result.methods or []
- if 'methods' in as_dict:
- method_strings = [x for x in as_dict['methods']]
+ if "methods" in as_dict:
+ method_strings = [x for x in as_dict["methods"]]
for key in as_dict:
for method in methods:
for method_string in method_strings:
@@ -352,29 +447,40 @@ def test_JavaScriptClass_from_dict(as_dict, error):
result = js_f.JavaScriptClass.from_dict(as_dict)
-@pytest.mark.parametrize('kwargs, expected, error', [
- ({'class_name': 'TestClass',
- 'methods': []},
- """class TestClass {\n}""",
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""function constructor() { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\n}""",
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor() { return true; }""",
- """testMethod(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
- None),
-
- ({},
- """class None {\n}""",
- errors.HighchartsMissingClassNameError),
-])
+@pytest.mark.parametrize(
+ "kwargs, expected, error",
+ [
+ ({"class_name": "TestClass", "methods": []}, """class TestClass {\n}""", None),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""function constructor() { return true; }"""],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\n}""",
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""constructor(test1, test2) { return true; }"""],
+ },
+ """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": [
+ """constructor() { return true; }""",
+ """testMethod(test1, test2) { return true; }""",
+ ],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ ({}, """class None {\n}""", errors.HighchartsMissingClassNameError),
+ ],
+)
def test_JavaScriptClass__str__(kwargs, expected, error):
instance = js_f.JavaScriptClass(**kwargs)
if not error:
@@ -389,56 +495,85 @@ def test_JavaScriptClass__str__(kwargs, expected, error):
with pytest.raises(error):
result = str(instance)
-@pytest.mark.parametrize('kwargs, expected, filename, error', [
- ({'class_name': 'TestClass',
- 'methods': []},
- """class TestClass {\n}""",
- None,
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""function constructor() { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\n}""",
- None,
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
- None,
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor() { return true; }""",
- """testMethod(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
- None,
- None),
-
- ({'class_name': 'TestClass',
- 'methods': []},
- """class TestClass {\n}""",
- 'test.js',
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""function constructor() { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\n}""",
- 'test.js',
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
- 'test.js',
- None),
- ({'class_name': 'TestClass',
- 'methods': ["""constructor() { return true; }""",
- """testMethod(test1, test2) { return true; }"""]},
- """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
- 'test.js',
- None),
-
- ({},
- """class None {\n}""",
- None,
- ValueError),
-])
+
+@pytest.mark.parametrize(
+ "kwargs, expected, filename, error",
+ [
+ (
+ {"class_name": "TestClass", "methods": []},
+ """class TestClass {\n}""",
+ None,
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""function constructor() { return true; }"""],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\n}""",
+ None,
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""constructor(test1, test2) { return true; }"""],
+ },
+ """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
+ None,
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": [
+ """constructor() { return true; }""",
+ """testMethod(test1, test2) { return true; }""",
+ ],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
+ None,
+ None,
+ ),
+ (
+ {"class_name": "TestClass", "methods": []},
+ """class TestClass {\n}""",
+ "test.js",
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""function constructor() { return true; }"""],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\n}""",
+ "test.js",
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": ["""constructor(test1, test2) { return true; }"""],
+ },
+ """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
+ "test.js",
+ None,
+ ),
+ (
+ {
+ "class_name": "TestClass",
+ "methods": [
+ """constructor() { return true; }""",
+ """testMethod(test1, test2) { return true; }""",
+ ],
+ },
+ """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
+ "test.js",
+ None,
+ ),
+ ({}, """class None {\n}""", None, ValueError),
+ ],
+)
def test_JavaScriptClass_to_js_literal(tmp_path, kwargs, expected, filename, error):
instance = js_f.JavaScriptClass(**kwargs)
if filename:
@@ -450,7 +585,7 @@ def test_JavaScriptClass_to_js_literal(tmp_path, kwargs, expected, filename, err
assert result == expected
if filename:
assert checkers.is_file(filename) is True
- with open(filename, 'r') as file_:
+ with open(filename, "r") as file_:
result_as_str = file_.read()
assert result_as_str == expected
else:
@@ -458,18 +593,26 @@ def test_JavaScriptClass_to_js_literal(tmp_path, kwargs, expected, filename, err
result = instance.to_js_literal(filename)
-@pytest.mark.parametrize('original_str, error', [
- ("""class TestClass {\n}""", None),
- ("""class TestClass {\nconstructor() {\n return true; \n}\n}""", None),
- ("""class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""", None),
- ("""class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""", None),
-
- ("""class None {\n}""", ValueError),
- ("""const notAClass = 123;""", errors.HighchartsParseError),
-])
+@pytest.mark.parametrize(
+ "original_str, error",
+ [
+ ("""class TestClass {\n}""", None),
+ ("""class TestClass {\nconstructor() {\n return true; \n}\n}""", None),
+ (
+ """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ (
+ """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ ("""class None {\n}""", ValueError),
+ ("""const notAClass = 123;""", errors.HighchartsParseError),
+ ],
+)
def test_JavaScriptClass_convert_from_js_ast(original_str, error):
original_parsed, updated_str = validate_js_function(original_str)
- unranged_result = validate_js_function(original_str, range = False)
+ unranged_result = validate_js_function(original_str, range=False)
unranged_parsed = unranged_result[0]
definition = original_parsed.body[0]
@@ -478,34 +621,90 @@ def test_JavaScriptClass_convert_from_js_ast(original_str, error):
assert result is not None
assert isinstance(result, js_f.JavaScriptClass) is True
as_str = str(result)
- as_str_parsed, updated_as_str = validate_js_function(as_str, range = False)
+ as_str_parsed, updated_as_str = validate_js_function(as_str, range=False)
assert str(as_str_parsed) == str(unranged_parsed)
else:
with pytest.raises(error):
result = js_f.JavaScriptClass._convert_from_js_ast(definition, original_str)
-@pytest.mark.parametrize('original_str, error', [
- ("""class TestClass {\n}""", None),
- ("""class TestClass {\nconstructor() {\n return true; \n}\n}""", None),
- ("""class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""", None),
- ("""class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""", None),
-
- ("""class None {\n}""", ValueError),
- ("""const notAClass = 123;""", errors.HighchartsParseError),
-])
+@pytest.mark.parametrize(
+ "original_str, error",
+ [
+ ("""class TestClass {\n}""", None),
+ ("""class TestClass {\nconstructor() {\n return true; \n}\n}""", None),
+ (
+ """class TestClass {\nconstructor(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ (
+ """class TestClass {\nconstructor() {\n return true; \n}\ntestMethod(test1,test2) {\n return true; \n}\n}""",
+ None,
+ ),
+ ("""class None {\n}""", ValueError),
+ ("""const notAClass = 123;""", errors.HighchartsParseError),
+ ],
+)
def test_JavaScriptClass_from_js_literal(original_str, error):
if not error:
- unranged_result = validate_js_function(original_str, range = False)
+ unranged_result = validate_js_function(original_str, range=False)
unranged_parsed = unranged_result[0]
result = js_f.JavaScriptClass.from_js_literal(original_str)
assert result is not None
assert isinstance(result, js_f.JavaScriptClass) is True
as_str = str(result)
- result_parsed = validate_js_function(as_str, range = False)
+ result_parsed = validate_js_function(as_str, range=False)
as_str_parsed = result_parsed[0]
assert str(as_str_parsed) == str(unranged_parsed)
else:
with pytest.raises(error):
result = js_f.JavaScriptClass.from_js_literal(original_str)
+
+
+@pytest.mark.parametrize(
+ "kwargs, expected, error",
+ [
+ ({}, """function() {}""", None),
+ (
+ {
+ "function_name": None,
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ """function(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": ["test1", "test2"],
+ "body": """return True;""",
+ },
+ """function testFunction(test1,test2) {\nreturn True;}""",
+ None,
+ ),
+ (
+ {
+ "function_name": "testFunction",
+ "arguments": None,
+ "body": """return "True";""",
+ },
+ """function testFunction() {\nreturn \\"True\\";}""",
+ None,
+ ),
+ ],
+)
+def test_bugfix213(kwargs, expected, error):
+ if not error:
+ obj = js_f.CallbackFunction(**kwargs)
+ assert obj is not None
+
+ as_json = obj.to_json(for_export=True)
+ print(as_json)
+ assert as_json == expected
+
+ else:
+ with pytest.raises(error):
+ obj = js_f.CallbackFunction(**kwargs)
+ as_json = obj.to_json(for_export=True)