From abf9d140d932cece4d29554593dde029d54a5469 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 08:59:34 -0500 Subject: [PATCH 1/9] Fixed missing hover state options. Closes #211. --- highcharts_core/utility_classes/states.py | 302 +++++++++++++++++----- 1 file changed, 237 insertions(+), 65 deletions(-) diff --git a/highcharts_core/utility_classes/states.py b/highcharts_core/utility_classes/states.py index ef993881..0b13d9f7 100644 --- a/highcharts_core/utility_classes/states.py +++ b/highcharts_core/utility_classes/states.py @@ -4,6 +4,7 @@ from validator_collection import validators from highcharts_core import errors +from highcharts_core.constants import EnforcedNull, EnforcedNullType from highcharts_core.decorators import class_sensitive, validate_types from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.utility_classes.animation import AnimationOptions @@ -11,6 +12,88 @@ from highcharts_core.utility_classes.patterns import Pattern +class Halo(HighchartsMeta): + """Options for configuring the halo state of a series or data point.""" + + def __init__(self, **kwargs): + self._attributes = None + self._opacity = None + self._size = None + + self.attributes = kwargs.get("attributes", None) + self.opacity = kwargs.get("opacity", None) + self.size = kwargs.get("size", None) + + @property + def attributes(self) -> Optional[dict]: + """A collection of SVG attributes to override the appearance of the halo, for example + ``fill``, ``stroke`` and ``stroke-width``. + + Attribute names should be provided as keys, and values should be provided as values. + + :rtype: :class:`dict ` + """ + return self._attributes + + @attributes.setter + def attributes(self, value: Optional[dict] = None): + if not value: + self._attributes = None + else: + value = validate_types(value, dict) + self._attributes = value + + @property + def opacity(self) -> Optional[int | float | Decimal]: + """Opacity for the halo unless a specific fill is overridden using the + :attr:`.attributes ` setting. + Defaults to ``0.25``. + + .. note:: + + Highcharts is only able to apply opacity to colors of hex or rgb(a) formats. + + :rtype: numeric + + """ + return self._opacity + + @opacity.setter + def opacity(self, value: Optional[int | float | Decimal] = None): + self._opacity = validators.numeric(value, allow_empty=True) + + @property + def size(self) -> Optional[int | float | Decimal]: + """The pixel size of the halo (or radius for point markers and width of the halo outside the bubble for bubbles). Defaults to ``10``, except for bubbles where it defaults to ``5``. + + :rtype: numeric + """ + return self._size + + @size.setter + def size(self, value: Optional[int | float | Decimal] = None): + self._size = validators.numeric(value, allow_empty=True) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + "attributes": as_dict.get("attributes", None), + "opacity": as_dict.get("opacity", None), + "size": as_dict.get("size", None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = { + "attributes": self.attributes, + "opacity": self.opacity, + "size": self.size, + } + + return untrimmed + + class HoverState(HighchartsMeta): """Options for the hovered point/series.""" @@ -20,12 +103,22 @@ def __init__(self, **kwargs): self._brightness = None self._color = None self._enabled = None + self._halo = None + self._line_width = None + self._line_width_plus = None + self._link_opacity = None + self._opacity = None - self.animation = kwargs.get('animation', None) - self.border_color = kwargs.get('border_color', None) - self.brightness = kwargs.get('brightness', None) - self.color = kwargs.get('color', None) - self.enabled = kwargs.get('enabled', None) + self.animation = kwargs.get("animation", None) + self.border_color = kwargs.get("border_color", None) + self.brightness = kwargs.get("brightness", None) + self.color = kwargs.get("color", None) + self.enabled = kwargs.get("enabled", None) + self.halo = kwargs.get("halo", None) + self.line_width = kwargs.get("line_width", None) + self.line_width_plus = kwargs.get("line_width_plus", None) + self.opacity = kwargs.get("opacity", None) + self.link_opacity = kwargs.get("link_opacity", None) @property def animation(self) -> Optional[AnimationOptions]: @@ -53,6 +146,7 @@ def border_color(self) -> Optional[str | Gradient | Pattern]: @border_color.setter def border_color(self, value): from highcharts_core import utility_functions + self._border_color = utility_functions.validate_color(value) @property @@ -67,7 +161,7 @@ def brightness(self) -> Optional[int | float | Decimal]: @brightness.setter def brightness(self, value): - self._brightness = validators.numeric(value, allow_empty = True) + self._brightness = validators.numeric(value, allow_empty=True) @property def color(self) -> Optional[str | Gradient | Pattern]: @@ -81,6 +175,7 @@ def color(self) -> Optional[str | Gradient | Pattern]: @color.setter def color(self, value): from highcharts_core import utility_functions + self._color = utility_functions.validate_color(value) @property @@ -93,7 +188,7 @@ def color(self) -> Optional[str]: @color.setter def color(self, value): - self._color = validators.string(value, allow_empty = True) + self._color = validators.string(value, allow_empty=True) @property def enabled(self) -> Optional[bool]: @@ -111,25 +206,105 @@ def enabled(self, value): else: self._enabled = bool(value) + @property + def halo(self) -> Optional[Halo | EnforcedNullType]: + """Options for configuring the halo that appears around the hovered point. + + .. note:: + + By default, the halo is filled by the current point or series color with an opacity of + ``0.25``. The halo can be disabled by setting the ``.halo`` property to :obj:`EnforcedNull `. + + """ + return self._halo + + @halo.setter + @class_sensitive(types=(Halo, EnforcedNullType)) + def halo(self, value: Optional[Halo | EnforcedNullType] = None): + self._halo = value + + @property + def line_width(self) -> Optional[int | float | Decimal]: + """Pixel width of the graph line. By default this property is :obj:`None `, + and the :attr:`.line_width_plus ` + property dictates how much to increase the line width from normal state. + + :rtype: numeric + """ + return self._line_width + + @line_width.setter + def line_width(self, value: Optional[int | float | Decimal] = None): + self._line_width = validators.numeric(value, allow_empty=True) + + @property + def line_width_plus(self) -> Optional[int | float | Decimal]: + """The additional line width for the graph line. Defaults to ``1``. + + :rtype: numeric + """ + return self._line_width_plus + + @line_width_plus.setter + def line_width_plus(self, value: Optional[int | float | Decimal] = None): + self._line_width_plus = validators.numeric(value, allow_empty=True) + + @property + def opacity(self) -> Optional[int | float | Decimal]: + """Opacity for nodes in the Sankey or related diagrams in hover mode. Defaults to + ``1``. + + :rtype: numeric + """ + return self._opacity + + @opacity.setter + def opacity(self, value: Optional[int | float | Decimal] = None): + self._opacity = validators.numeric(value, allow_empty=True) + + @property + def link_opacity(self) -> Optional[int | float | Decimal]: + """Opacity for the links between nodes in Sankey or related diagrams in hover mode. + + Defaults to ``1``. + + :rtype: numeric + """ + return self._link_opacity + + @link_opacity.setter + def link_opacity(self, value: Optional[int | float | Decimal] = None): + self._link_opacity = validators.numeric(value, allow_empty=True) + @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'animation': as_dict.get('animation', None), - 'border_color': as_dict.get('borderColor', None), - 'brightness': as_dict.get('brightness', None), - 'color': as_dict.get('color', None), - 'enabled': as_dict.get('enabled', None) + "animation": as_dict.get("animation", None), + "border_color": as_dict.get("borderColor", None), + "brightness": as_dict.get("brightness", None), + "color": as_dict.get("color", None), + "enabled": as_dict.get("enabled", None), + "halo": as_dict.get("halo", None), + "line_width": as_dict.get("lineWidth", None), + "line_width_plus": as_dict.get("lineWidthPlus", None), + "opacity": as_dict.get("opacity", None), + "link_opacity": as_dict.get("linkOpacity", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'animation': self.animation, - 'borderColor': self.border_color, - 'brightness': self.brightness, - 'color': self.color, - 'enabled': self.enabled + "animation": self.animation, + "borderColor": self.border_color, + "brightness": self.brightness, + "color": self.color, + "enabled": self.enabled, + "halo": self.halo, + "lineWidth": self.line_width, + "lineWidthPlus": self.line_width_plus, + "opacity": self.opacity, + "linkOpacity": self.link_opacity, } return untrimmed @@ -143,9 +318,9 @@ def __init__(self, **kwargs): self._enabled = None self._opacity = None - self.animation = kwargs.get('animation', None) - self.enabled = kwargs.get('enabled', None) - self.opacity = kwargs.get('opacity', None) + self.animation = kwargs.get("animation", None) + self.enabled = kwargs.get("enabled", None) + self.opacity = kwargs.get("opacity", None) @property def animation(self) -> Optional[AnimationOptions]: @@ -185,23 +360,23 @@ def opacity(self) -> Optional[int | float | Decimal]: @opacity.setter def opacity(self, value): - self._opacity = validators.numeric(value, allow_empty = True) + self._opacity = validators.numeric(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'animation': as_dict.get('animation', None), - 'enabled': as_dict.get('enabled', None), - 'opacity': as_dict.get('opacity', None) + "animation": as_dict.get("animation", None), + "enabled": as_dict.get("enabled", None), + "opacity": as_dict.get("opacity", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'animation': self.animation, - 'enabled': self.enabled, - 'opacity': self.opacity + "animation": self.animation, + "enabled": self.enabled, + "opacity": self.opacity, } return untrimmed @@ -213,7 +388,7 @@ class NormalState(HighchartsMeta): def __init__(self, **kwargs): self._animation = None - self.animation = kwargs.get('animation', None) + self.animation = kwargs.get("animation", None) @property def animation(self) -> Optional[bool | AnimationOptions]: @@ -230,21 +405,16 @@ def animation(self, value): if isinstance(value, bool): self._animation = value else: - self._animation = validate_types(value, - types = AnimationOptions) + self._animation = validate_types(value, types=AnimationOptions) @classmethod def _get_kwargs_from_dict(cls, as_dict): - kwargs = { - 'animation': as_dict.get('animation', None) - } + kwargs = {"animation": as_dict.get("animation", None)} return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: - untrimmed = { - 'animation': self.animation - } + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = {"animation": self.animation} return untrimmed @@ -259,10 +429,10 @@ def __init__(self, **kwargs): self._color = None self._enabled = None - self.animation = kwargs.get('animation', None) - self.border_color = kwargs.get('border_color', None) - self.color = kwargs.get('color', None) - self.enabled = kwargs.get('enabled', None) + self.animation = kwargs.get("animation", None) + self.border_color = kwargs.get("border_color", None) + self.color = kwargs.get("color", None) + self.enabled = kwargs.get("enabled", None) @property def animation(self) -> Optional[AnimationOptions]: @@ -289,6 +459,7 @@ def border_color(self) -> Optional[str | Gradient | Pattern]: @border_color.setter def border_color(self, value): from highcharts_core import utility_functions + self._border_color = utility_functions.validate_color(value) @property @@ -303,6 +474,7 @@ def color(self) -> Optional[str | Gradient | Pattern]: @color.setter def color(self, value): from highcharts_core import utility_functions + self._color = utility_functions.validate_color(value) @property @@ -324,20 +496,20 @@ def enabled(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'animation': as_dict.get('animation', None), - 'border_color': as_dict.get('borderColor', None), - 'color': as_dict.get('color', None), - 'enabled': as_dict.get('enabled', None), + "animation": as_dict.get("animation", None), + "border_color": as_dict.get("borderColor", None), + "color": as_dict.get("color", None), + "enabled": as_dict.get("enabled", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'animation': self.animation, - 'borderColor': self.border_color, - 'color': self.color, - 'enabled': self.enabled + "animation": self.animation, + "borderColor": self.border_color, + "color": self.color, + "enabled": self.enabled, } return untrimmed @@ -353,10 +525,10 @@ def __init__(self, **kwargs): self._normal = None self._select = None - self.hover = kwargs.get('hover', None) - self.inactive = kwargs.get('inactive', None) - self.normal = kwargs.get('normal', None) - self.select = kwargs.get('select', None) + self.hover = kwargs.get("hover", None) + self.inactive = kwargs.get("inactive", None) + self.normal = kwargs.get("normal", None) + self.select = kwargs.get("select", None) @property def hover(self) -> Optional[HoverState]: @@ -424,20 +596,20 @@ def select(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'hover': as_dict.get('hover', None), - 'inactive': as_dict.get('inactive', None), - 'normal': as_dict.get('normal', None), - 'select': as_dict.get('select', None) + "hover": as_dict.get("hover", None), + "inactive": as_dict.get("inactive", None), + "normal": as_dict.get("normal", None), + "select": as_dict.get("select", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'hover': self.hover, - 'inactive': self.inactive, - 'normal': self.normal, - 'select': self.select + "hover": self.hover, + "inactive": self.inactive, + "normal": self.normal, + "select": self.select, } return untrimmed From e20b81ae483d0346aa19485e0f19deb5207142cf Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 08:59:47 -0500 Subject: [PATCH 2/9] Bumped version number and updated changelog. --- CHANGES.rst | 8 ++++++++ highcharts_core/__version__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c5204e5a..967ba4cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,12 @@ +Release 1.10.3 +========================================= + +* **BUGFIX:** Fixed support for missing ``HoverState`` options. Closes #211. + +---- + + Release 1.10.2 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index 83b8f26c..bc8f1165 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = "1.10.2" +__version__ = "1.10.3" From 0f1e1203a895e7e5c968ea9e8656bf917ff1f8b3 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 09:40:07 -0500 Subject: [PATCH 3/9] Fixed JSON quotation mark escaping. --- .../utility_classes/javascript_functions.py | 319 +++++++++--------- 1 file changed, 164 insertions(+), 155 deletions(-) diff --git a/highcharts_core/utility_classes/javascript_functions.py b/highcharts_core/utility_classes/javascript_functions.py index cfff0ada..696bb5ce 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 From bdb51e436ef9649a3bf38d8eeb61bf25856152b7 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 09:40:35 -0500 Subject: [PATCH 4/9] Added unit tests --- .../test_javascript_functions.py | 707 +++++++++++------- 1 file changed, 453 insertions(+), 254 deletions(-) diff --git a/tests/utility_classes/test_javascript_functions.py b/tests/utility_classes/test_javascript_functions.py index d18391bd..da5b85bb 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) From 89222f8b094ae157260ac69f0229ff398212f4a1 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 09:40:44 -0500 Subject: [PATCH 5/9] Updated changelog. --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 967ba4cd..c21d700b 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. ---- From 11d4c7236cc562a6fd9e929c651c9fd5ee229348 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 10:10:35 -0500 Subject: [PATCH 6/9] Fixed Network Graph data label properties. --- .../options/plot_options/networkgraph.py | 310 ++++++++++-------- highcharts_core/utility_classes/states.py | 3 +- 2 files changed, 169 insertions(+), 144 deletions(-) diff --git a/highcharts_core/options/plot_options/networkgraph.py b/highcharts_core/options/plot_options/networkgraph.py index 21e28c6d..71415dd5 100644 --- a/highcharts_core/options/plot_options/networkgraph.py +++ b/highcharts_core/options/plot_options/networkgraph.py @@ -1,13 +1,14 @@ from typing import Optional, List from decimal import Decimal -from validator_collection import validators +from validator_collection import validators, checkers from highcharts_core import errors from highcharts_core.decorators import class_sensitive, validate_types from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.options.plot_options.generic import GenericTypeOptions from highcharts_core.options.plot_options.link import LinkOptions +from highcharts_core.utility_classes.data_labels import OrganizationDataLabel from highcharts_core.utility_classes.zones import Zone from highcharts_core.utility_classes.shadows import ShadowOptions from highcharts_core.utility_classes.javascript_functions import CallbackFunction @@ -33,20 +34,20 @@ def __init__(self, **kwargs): self._theta = None self._type = None - self.approximation = kwargs.get('approximation', None) - self.attractive_force = kwargs.get('attractive_force', None) - self.enable_simulation = kwargs.get('enable_simulation', None) - self.friction = kwargs.get('friction', None) - self.gravitational_constant = kwargs.get('gravitational_constant', None) - self.initial_position_radius = kwargs.get('initial_position_radius', None) - self.initial_positions = kwargs.get('initial_positions', None) - self.integration = kwargs.get('integration', None) - self.link_length = kwargs.get('link_length', None) - self.max_iterations = kwargs.get('max_iterations', None) - self.max_speed = kwargs.get('max_speed', None) - self.repulsive_force = kwargs.get('repulsive_force', None) - self.theta = kwargs.get('theta', None) - self.type = kwargs.get('type', None) + self.approximation = kwargs.get("approximation", None) + self.attractive_force = kwargs.get("attractive_force", None) + self.enable_simulation = kwargs.get("enable_simulation", None) + self.friction = kwargs.get("friction", None) + self.gravitational_constant = kwargs.get("gravitational_constant", None) + self.initial_position_radius = kwargs.get("initial_position_radius", None) + self.initial_positions = kwargs.get("initial_positions", None) + self.integration = kwargs.get("integration", None) + self.link_length = kwargs.get("link_length", None) + self.max_iterations = kwargs.get("max_iterations", None) + self.max_speed = kwargs.get("max_speed", None) + self.repulsive_force = kwargs.get("repulsive_force", None) + self.theta = kwargs.get("theta", None) + self.type = kwargs.get("type", None) @property def approximation(self) -> Optional[str]: @@ -69,7 +70,7 @@ def approximation(self) -> Optional[str]: @approximation.setter def approximation(self, value): - self._approximation = validators.string(value, allow_empty = True) + self._approximation = validators.string(value, allow_empty=True) @property def attractive_force(self) -> Optional[CallbackFunction]: @@ -138,7 +139,7 @@ def friction(self) -> Optional[int | float | Decimal]: @friction.setter def friction(self, value): - self._friction = validators.numeric(value, allow_empty = True) + self._friction = validators.numeric(value, allow_empty=True) @property def gravitational_constant(self) -> Optional[int | float | Decimal]: @@ -151,7 +152,7 @@ def gravitational_constant(self) -> Optional[int | float | Decimal]: @gravitational_constant.setter def gravitational_constant(self, value): - self._gravitational_constant = validators.numeric(value, allow_empty = True) + self._gravitational_constant = validators.numeric(value, allow_empty=True) @property def initial_position_radius(self) -> Optional[int | float | Decimal]: @@ -165,7 +166,7 @@ def initial_position_radius(self) -> Optional[int | float | Decimal]: @initial_position_radius.setter def initial_position_radius(self, value): - self._initial_position_radius = validators.numeric(value, allow_empty = True) + self._initial_position_radius = validators.numeric(value, allow_empty=True) @property def initial_positions(self) -> Optional[str]: @@ -184,7 +185,7 @@ def initial_positions(self) -> Optional[str]: @initial_positions.setter def initial_positions(self, value): - self._initial_positions = validators.string(value, allow_empty = True) + self._initial_positions = validators.string(value, allow_empty=True) @property def integration(self) -> Optional[str]: @@ -213,9 +214,10 @@ def integration(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['euler', 'verlet']: - raise errors.HighchartsValueError(f'integration expects either "euler" ' - f'or "verlet". Was: {value}') + if value not in ["euler", "verlet"]: + raise errors.HighchartsValueError( + f'integration expects either "euler" or "verlet". Was: {value}' + ) self._integration = value @property @@ -235,9 +237,7 @@ def link_length(self) -> Optional[int | float | Decimal]: @link_length.setter def link_length(self, value): - self._link_length = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._link_length = validators.numeric(value, allow_empty=True, minimum=0) @property def max_iterations(self) -> Optional[int]: @@ -254,9 +254,7 @@ def max_iterations(self) -> Optional[int]: @max_iterations.setter def max_iterations(self, value): - self._max_iterations = validators.integer(value, - allow_empty = True, - minimum = 1) + self._max_iterations = validators.integer(value, allow_empty=True, minimum=1) @property def max_speed(self) -> Optional[int | float | Decimal]: @@ -273,9 +271,7 @@ def max_speed(self) -> Optional[int | float | Decimal]: @max_speed.setter def max_speed(self, value): - self._max_speed = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._max_speed = validators.numeric(value, allow_empty=True, minimum=0) @property def repulsive_force(self) -> Optional[CallbackFunction]: @@ -332,7 +328,7 @@ def theta(self) -> Optional[int | float | Decimal]: @theta.setter def theta(self, value): - self._theta = validators.numeric(value, allow_empty = True) + self._theta = validators.numeric(value, allow_empty=True) @property def type(self) -> Optional[str]: @@ -345,45 +341,45 @@ def type(self) -> Optional[str]: @type.setter def type(self, value): - self._type = validators.string(value, allow_empty = True) + self._type = validators.string(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'approximation': as_dict.get('approximation', None), - 'attractive_force': as_dict.get('attractiveForce', None), - 'enable_simulation': as_dict.get('enableSimulation', None), - 'friction': as_dict.get('friction', None), - 'gravitational_constant': as_dict.get('gravitationalConstant', None), - 'initial_position_radius': as_dict.get('initialPositionRadius', None), - 'initial_positions': as_dict.get('initialPositions', None), - 'integration': as_dict.get('integration', None), - 'link_length': as_dict.get('linkLength', None), - 'max_iterations': as_dict.get('maxIterations', None), - 'max_speed': as_dict.get('maxSpeed', None), - 'repulsive_force': as_dict.get('repulsiveForce', None), - 'theta': as_dict.get('theta', None), - 'type': as_dict.get('type', None) + "approximation": as_dict.get("approximation", None), + "attractive_force": as_dict.get("attractiveForce", None), + "enable_simulation": as_dict.get("enableSimulation", None), + "friction": as_dict.get("friction", None), + "gravitational_constant": as_dict.get("gravitationalConstant", None), + "initial_position_radius": as_dict.get("initialPositionRadius", None), + "initial_positions": as_dict.get("initialPositions", None), + "integration": as_dict.get("integration", None), + "link_length": as_dict.get("linkLength", None), + "max_iterations": as_dict.get("maxIterations", None), + "max_speed": as_dict.get("maxSpeed", None), + "repulsive_force": as_dict.get("repulsiveForce", None), + "theta": as_dict.get("theta", None), + "type": as_dict.get("type", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'approximation': self.approximation, - 'attractiveForce': self.attractive_force, - 'enableSimulation': self.enable_simulation, - 'friction': self.friction, - 'gravitationalConstant': self.gravitational_constant, - 'initialPositionRadius': self.initial_position_radius, - 'initialPositions': self.initial_positions, - 'integration': self.integration, - 'linkLength': self.link_length, - 'maxIterations': self.max_iterations, - 'maxSpeed': self.max_speed, - 'repulsiveForce': self.repulsive_force, - 'theta': self.theta, - 'type': self.type + "approximation": self.approximation, + "attractiveForce": self.attractive_force, + "enableSimulation": self.enable_simulation, + "friction": self.friction, + "gravitationalConstant": self.gravitational_constant, + "initialPositionRadius": self.initial_position_radius, + "initialPositions": self.initial_positions, + "integration": self.integration, + "linkLength": self.link_length, + "maxIterations": self.max_iterations, + "maxSpeed": self.max_speed, + "repulsiveForce": self.repulsive_force, + "theta": self.theta, + "type": self.type, } return untrimmed @@ -413,16 +409,16 @@ def __init__(self, **kwargs): self._shadow = None self._zones = None - self.color_index = kwargs.get('color_index', None) - self.crisp = kwargs.get('crisp', None) - self.draggable = kwargs.get('draggable', None) - self.find_nearest_point_by = kwargs.get('find_nearest_point_by', None) - self.layout_algorithm = kwargs.get('layout_algorithm', None) - self.line_width = kwargs.get('line_width', None) - self.link = kwargs.get('link', None) - self.relative_x_value = kwargs.get('relative_x_value', None) - self.shadow = kwargs.get('shadow', None) - self.zones = kwargs.get('zones', None) + self.color_index = kwargs.get("color_index", None) + self.crisp = kwargs.get("crisp", None) + self.draggable = kwargs.get("draggable", None) + self.find_nearest_point_by = kwargs.get("find_nearest_point_by", None) + self.layout_algorithm = kwargs.get("layout_algorithm", None) + self.line_width = kwargs.get("line_width", None) + self.link = kwargs.get("link", None) + self.relative_x_value = kwargs.get("relative_x_value", None) + self.shadow = kwargs.get("shadow", None) + self.zones = kwargs.get("zones", None) super().__init__(**kwargs) @@ -440,9 +436,7 @@ def color_index(self) -> Optional[int]: @color_index.setter def color_index(self, value): - self._color_index = validators.integer(value, - allow_empty = True, - minimum = 0) + self._color_index = validators.integer(value, allow_empty=True, minimum=0) @property def crisp(self) -> Optional[bool]: @@ -467,6 +461,41 @@ def crisp(self, value): else: self._crisp = bool(value) + @property + def data_labels( + self, + ) -> Optional[OrganizationDataLabel | List[OrganizationDataLabel]]: + """Options for the series data labels, appearing next to each data point. + + .. note:: + + To have multiple data labels per data point, you can also supply a collection of + :class:`DataLabel` configuration settings. + + :rtype: :class:`OrganizationDataLabel `, + :class:`list ` of + :class:`OrganizationDataLabel ` or + :obj:`None ` + """ + return self._data_labels + + @data_labels.setter + def data_labels(self, value): + if not value: + self._data_labels = None + else: + if checkers.is_iterable(value): + self._data_labels = validate_types( + value, + types=OrganizationDataLabel, + allow_none=False, + force_iterable=True, + ) + else: + self._data_labels = validate_types( + value, types=OrganizationDataLabel, allow_none=False + ) + @property def draggable(self) -> Optional[bool]: """If ``True``, indicates that the nodes are draggable. Defaults to ``True``. @@ -491,7 +520,7 @@ def events(self) -> Optional[SimulationEvents]: These event hooks can also be attached to the series at run time using the (JavaScript) ``Highcharts.addEvent()`` function. - :rtype: :class:`SimulationEvents ` or + :rtype: :class:`SimulationEvents ` or :obj:`None ` """ return self._events @@ -519,7 +548,7 @@ def find_nearest_point_by(self) -> Optional[str]: @find_nearest_point_by.setter def find_nearest_point_by(self, value): - self._find_nearest_point_by = validators.string(value, allow_empty = True) + self._find_nearest_point_by = validators.string(value, allow_empty=True) @property def layout_algorithm(self) -> Optional[LayoutAlgorithm]: @@ -544,9 +573,7 @@ def line_width(self) -> Optional[int | float | Decimal]: @line_width.setter def line_width(self, value): - self._line_width = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._line_width = validators.numeric(value, allow_empty=True, minimum=0) @property def link(self) -> Optional[LinkOptions]: @@ -606,8 +633,7 @@ def shadow(self, value): elif not value: self._shadow = None else: - value = validate_types(value, - types = ShadowOptions) + value = validate_types(value, types=ShadowOptions) self._shadow = value @property @@ -627,77 +653,77 @@ def zones(self) -> Optional[List[Zone]]: return self._zones @zones.setter - @class_sensitive(Zone, - force_iterable = True) + @class_sensitive(Zone, force_iterable=True) def zones(self, value): self._zones = value @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - 'threshold': as_dict.get('threshold', None), - 'tooltip': as_dict.get('tooltip', None), - 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', None), - - 'color_index': as_dict.get('colorIndex', None), - 'crisp': as_dict.get('crisp', None), - 'draggable': as_dict.get('draggable', None), - 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), - 'layout_algorithm': as_dict.get('layoutAlgorithm', None), - 'line_width': as_dict.get('lineWidth', None), - 'link': as_dict.get('link', None), - 'relative_x_value': as_dict.get('relativeXValue', None), - 'shadow': as_dict.get('shadow', None), - 'zones': as_dict.get('zones', None) + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "threshold": as_dict.get("threshold", None), + "tooltip": as_dict.get("tooltip", None), + "turbo_threshold": as_dict.get("turboThreshold", None), + "visible": as_dict.get("visible", None), + "color_index": as_dict.get("colorIndex", None), + "crisp": as_dict.get("crisp", None), + "draggable": as_dict.get("draggable", None), + "find_nearest_point_by": as_dict.get("findNearestPointBy", None), + "layout_algorithm": as_dict.get("layoutAlgorithm", None), + "line_width": as_dict.get("lineWidth", None), + "link": as_dict.get("link", None), + "relative_x_value": as_dict.get("relativeXValue", None), + "shadow": as_dict.get("shadow", None), + "zones": as_dict.get("zones", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'colorIndex': self.color_index, - 'crisp': self.crisp, - 'draggable': self.draggable, - 'findNearestPointBy': self.find_nearest_point_by, - 'layoutAlgorithm': self.layout_algorithm, - 'lineWidth': self.line_width, - 'link': self.link, - 'relativeXValue': self.relative_x_value, - 'shadow': self.shadow, - 'zones': self.zones + "colorIndex": self.color_index, + "crisp": self.crisp, + "draggable": self.draggable, + "findNearestPointBy": self.find_nearest_point_by, + "layoutAlgorithm": self.layout_algorithm, + "lineWidth": self.line_width, + "link": self.link, + "relativeXValue": self.relative_x_value, + "shadow": self.shadow, + "zones": self.zones, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] diff --git a/highcharts_core/utility_classes/states.py b/highcharts_core/utility_classes/states.py index 0b13d9f7..00530779 100644 --- a/highcharts_core/utility_classes/states.py +++ b/highcharts_core/utility_classes/states.py @@ -3,8 +3,7 @@ from validator_collection import validators -from highcharts_core import errors -from highcharts_core.constants import EnforcedNull, EnforcedNullType +from highcharts_core.constants import EnforcedNullType from highcharts_core.decorators import class_sensitive, validate_types from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.utility_classes.animation import AnimationOptions From dce3f46fd0b59a35a742807ad9f8ec14da1ccfb9 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 10:12:50 -0500 Subject: [PATCH 7/9] Updated changelog. --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index c21d700b..3e5c9e21 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Release 1.10.3 * **BUGFIX:** Fixed support for missing ``HoverState`` options. Closes #211. * **BUGFIX:** Fixed JavaScript serialization of ``CallbackFunction``. Closes #213. +* **BUGFIX:** Fixed the ``DataLabel`` support for `NetworkGraphOptions`. Closes #215. ---- From 343d05713ba91c28b46ce709d995b3b7f2f43b6a Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 11:38:42 -0500 Subject: [PATCH 8/9] Updated dependencies to fix Sphinx Toolbox issue. --- pyproject.toml | 4 ++-- requirements.dev.numpy.txt | 2 +- requirements.dev.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6824706..e984f3ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ dev = [ "pytz>=2022.1", "Sphinx==6.1.3", "sphinx-rtd-theme==1.2.0", - "sphinx-toolbox==3.4.0", + "sphinx-toolbox==3.6.0", "sphinx-tabs==3.4.1", "tox>=4.0.0", "IPython>=8.10.0", @@ -109,6 +109,6 @@ ai = [ docs = [ "Sphinx==6.1.3", "sphinx-rtd-theme==1.2.0", - "sphinx-toolbox==3.4.0", + "sphinx-toolbox==3.6.0", "sphinx-tabs==3.4.1" ] \ No newline at end of file diff --git a/requirements.dev.numpy.txt b/requirements.dev.numpy.txt index c40c2563..25f60460 100644 --- a/requirements.dev.numpy.txt +++ b/requirements.dev.numpy.txt @@ -6,7 +6,7 @@ python-dotenv>=0.20.0 pytz==2022.1 Sphinx==6.1.3 sphinx-rtd-theme==1.2.0 -sphinx-toolbox==3.4.0 +sphinx-toolbox==3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 requests==2.32.0 diff --git a/requirements.dev.txt b/requirements.dev.txt index 3edfb736..73b0a398 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -6,7 +6,7 @@ python-dotenv>=0.20.0 pytz==2022.1 Sphinx==6.1.3 sphinx-rtd-theme==1.2.0 -sphinx-toolbox==3.4.0 +sphinx-toolbox==3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 requests==2.32.0 From ec2afc09a2f04ad8a69d6d518d43672934808d26 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Feb 2025 13:59:34 -0500 Subject: [PATCH 9/9] Updated Sphinx Toolbox version requirement. --- pyproject.toml | 4 ++-- requirements.dev.numpy.txt | 2 +- requirements.dev.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e984f3ba..2c1b2302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ dev = [ "pytz>=2022.1", "Sphinx==6.1.3", "sphinx-rtd-theme==1.2.0", - "sphinx-toolbox==3.6.0", + "sphinx-toolbox>=3.6.0", "sphinx-tabs==3.4.1", "tox>=4.0.0", "IPython>=8.10.0", @@ -109,6 +109,6 @@ ai = [ docs = [ "Sphinx==6.1.3", "sphinx-rtd-theme==1.2.0", - "sphinx-toolbox==3.6.0", + "sphinx-toolbox>=3.6.0", "sphinx-tabs==3.4.1" ] \ No newline at end of file diff --git a/requirements.dev.numpy.txt b/requirements.dev.numpy.txt index 25f60460..f223333d 100644 --- a/requirements.dev.numpy.txt +++ b/requirements.dev.numpy.txt @@ -6,7 +6,7 @@ python-dotenv>=0.20.0 pytz==2022.1 Sphinx==6.1.3 sphinx-rtd-theme==1.2.0 -sphinx-toolbox==3.6.0 +sphinx-toolbox>=3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 requests==2.32.0 diff --git a/requirements.dev.txt b/requirements.dev.txt index 73b0a398..0a46f6f9 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -6,7 +6,7 @@ python-dotenv>=0.20.0 pytz==2022.1 Sphinx==6.1.3 sphinx-rtd-theme==1.2.0 -sphinx-toolbox==3.6.0 +sphinx-toolbox>=3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 requests==2.32.0