Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@

Release 1.3.4
=========================================

* **ENHANCEMENT:** Converted `ButtonTheme` into an extensible descendent of `JavaScriptDict` (#86).

---------------------

Release 1.3.3
=========================================

Expand Down
2 changes: 1 addition & 1 deletion highcharts_core/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.3.3'
__version__ = '1.3.4'
105 changes: 100 additions & 5 deletions highcharts_core/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,14 +796,20 @@ def __setitem__(self, key, item):
super().__setitem__(key, item)

@classmethod
def from_dict(cls, as_dict):
def from_dict(cls,
as_dict: dict,
allow_snake_case: bool = True):
"""Construct an instance of the class from a :class:`dict <python:dict>` object.

:param as_dict: A :class:`dict <python:dict>` representation of the object.
:type as_dict: :class:`dict <python:dict>`

:param allow_snake_case: If ``True``, interprets ``snake_case`` keys as equivalent
to ``camelCase`` keys. Defaults to ``True``.
:type allow_snake_case: :class:`bool <python:bool>`

:returns: A Python object representation of ``as_dict``.
:rtype: :class:`JavaScriptDict`
:rtype: :class:`HighchartsMeta`
"""
as_dict = validators.dict(as_dict, allow_empty = True)
if not as_dict:
Expand Down Expand Up @@ -1120,9 +1126,98 @@ def _validate_js_literal(cls,
range = range,
_break_loop_on_failure = True)
else:
raise errors.HighchartsParseError('._validate_js_function() expects '
raise errors.HighchartsParseError('._validate_js_literal() expects '
'a str containing a valid '
'JavaScript function. Could not '
'find a valid function.')
'JavaScript literal object. Could '
'not find a valid JS literal '
'object.')

return parsed, as_str

@classmethod
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.

:param as_str_or_file: The JavaScript object literal, represented either as a
:class:`str <python:str>` or as a filename which contains the JS object literal.
:type as_str_or_file: :class:`str <python:str>`

:param allow_snake_case: If ``True``, interprets ``snake_case`` keys as equivalent
to ``camelCase`` keys. Defaults to ``True``.
:type allow_snake_case: :class:`bool <python:bool>`

:param _break_loop_on_failure: If ``True``, will break any looping operations in
the event of a failure. Otherwise, will attempt to repair the failure. Defaults
to ``False``.
:type _break_loop_on_failure: :class:`bool <python:bool>`

:returns: A Python object representation of the Highcharts JavaScript object
literal.
:rtype: :class:`HighchartsMeta`
"""
is_file = checkers.is_file(as_str_or_file)
if is_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_literal(as_str)

as_dict = {}
if not parsed.body:
return cls()

if len(parsed.body) > 1:
raise errors.HighchartsCollectionError(f'each JavaScript object literal is '
f'expected to contain one object. '
f'However, you attempted to parse '
f'{len(parsed.body)} objects.')

body = parsed.body[0]
if not checkers.is_type(body, 'VariableDeclaration') and \
_break_loop_on_failure is False:
prefixed_str = f'var randomVariable = {as_str}'
return cls.from_js_literal(prefixed_str,
_break_loop_on_failure = True)
elif not checkers.is_type(body, 'VariableDeclaration'):
raise errors.HighchartsVariableDeclarationError('To parse a JavaScriot '
'object literal, it is '
'expected to be either a '
'variable declaration or a'
'standalone block statement.'
'Input received did not '
'conform.')
declarations = body.declarations
if not declarations:
return cls()

if len(declarations) > 1:
raise errors.HighchartsCollectionError(f'each JavaScript object literal is '
f'expected to contain one object. '
f'However, you attempted to parse '
f'{len(parsed.body)} objects.')
object_expression = declarations[0].init
if not checkers.is_type(object_expression, 'ObjectExpression'):
raise errors.HighchartsParseError(f'Highcharts expects an object literal to '
f'to be defined as a standard '
f'ObjectExpression. Received: '
f'{type(object_expression)}')

properties = object_expression.properties
if not properties:
return cls()

key_value_pairs = [(x[0], x[1]) for x in get_key_value_pairs(properties,
updated_str)]

for pair in key_value_pairs:
as_dict[pair[0]] = pair[1]

return cls.from_dict(as_dict,
allow_snake_case = allow_snake_case)

93 changes: 78 additions & 15 deletions highcharts_core/utility_classes/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,57 @@
from highcharts_core.utility_classes.gradients import Gradient
from highcharts_core.utility_classes.patterns import Pattern
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
from highcharts_core.utility_classes.ast import AttributeObject
from highcharts_core.utility_classes.states import States


class ButtonTheme(HighchartsMeta):
class ButtonTheme(AttributeObject):
"""Settings used to style buttons."""

def __init__(self, **kwargs):
self._fill = None
self._stroke = None
trimmed_kwargs = {x: y for x, y in kwargs.items() if not hasattr(self, x)}
super().__init__(**trimmed_kwargs)

self.fill = kwargs.get('fill', None)
self.padding = kwargs.get('padding', None)
self.stroke = kwargs.get('stroke', None)
self.states = kwargs.get('states', None)

def __setitem__(self, key, item):
validate_key = False
try:
validate_key = key not in self
except AttributeError:
validate_key = True

if validate_key:
try:
key = validators.variable_name(key, allow_empty = False)
except validator_errors.InvalidVariableNameError as error:
if '-' in key:
try:
test_key = key.replace('-', '_')
validators.variable_name(test_key, allow_empty = False)
except validator_errors.InvalidVariableNameError:
raise error
else:
raise error

if self._valid_value_types:
try:
item = validate_types(item,
types = self._valid_value_types,
allow_none = self._allow_empty_value)
except errors.HighchartsValueError as error:
if self._allow_empty_value and not item:
item = None
else:
try:
item = self._valid_value_types(item)
except (TypeError, ValueError, AttributeError):
raise error

super().__setitem__(key, item)

@property
def fill(self) -> Optional[str | Gradient | Pattern]:
Expand All @@ -40,41 +80,64 @@ def fill(self) -> Optional[str | Gradient | Pattern]:
:rtype: :class:`str <python:str>` (for colors), :class:`Gradient` for gradients,
:class:`Pattern` for pattern definitions, or :obj:`None <python:None>`
"""
return self._fill
return self.get('fill', None)

@fill.setter
def fill(self, value):
self._fill = utility_functions.validate_color(value)
self['fill'] = utility_functions.validate_color(value)

@property
def padding(self) -> Optional[int | float | Decimal]:
"""Padding for the button. Defaults to `5`.

:rtype: numeric
"""
return self.get('padding', None)

@padding.setter
def padding(self, value):
self['padding'] = validators.numeric(value, allow_empty = True)

@property
def states(self) -> Optional[States]:
"""States to apply to the button. Defaults to :obj:`None <python:None>`.

:rtype: :class:`States <highcharts_core.utility_classes.states.States>` or
:obj:`None <python:None>`
"""
return self.get('states', None)

@states.setter
def states(self, value):
self['states'] = validate_types(value, States)

@property
def stroke(self) -> Optional[str]:
"""The color of the button's stroke. Defaults to ``'none'``.

:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._stroke
return self.get('stroke', None)

@stroke.setter
def stroke(self, value):
self._stroke = validators.string(value, allow_empty = True)
self['stroke'] = validators.string(value, allow_empty = True)

@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'fill': as_dict.get('fill', None),
'padding': as_dict.get('padding', None),
'states': as_dict.get('states', None),
'stroke': as_dict.get('stroke', None)
}

for key in as_dict:
if key not in kwargs:
kwargs[key] = as_dict.get(key, None)

return kwargs

def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'fill': self.fill,
'stroke': self.stroke
}

return untrimmed


class ButtonConfiguration(HighchartsMeta):
"""Configuration of options that apply to a given button."""
Expand Down
26 changes: 17 additions & 9 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def does_kwarg_value_match_result(kwarg_value, result_value):
return True


def trim_expected(expected):
def trim_expected_dict(expected):
"""Remove keys from ``expected`` or its children that should not be evaluated."""
new_dict = {}
if not isinstance(expected, dict):
Expand All @@ -264,13 +264,13 @@ def trim_expected(expected):
if expected[key] is None:
continue
elif isinstance(expected[key], dict):
trimmed_value = trim_expected(expected[key])
trimmed_value = trim_expected_dict(expected[key])
if trimmed_value:
new_dict[key] = trimmed_value
elif checkers.is_iterable(expected[key]):
trimmed_value = []
for item in expected[key]:
trimmed_item = trim_expected(item)
trimmed_item = trim_expected_dict(item)
if trimmed_item:
trimmed_value.append(trimmed_item)

Expand Down Expand Up @@ -314,7 +314,7 @@ def compare_js_literals(original, new):
return True


def Class__init__(cls, kwargs, error):
def Class__init__(cls, kwargs, error, check_as_dict = False):
kwargs_copy = deepcopy(kwargs)
if not error:
result = cls(**kwargs)
Expand Down Expand Up @@ -365,11 +365,14 @@ def Class__init__(cls, kwargs, error):
else:
print('not margin')
kwarg_value = kwargs_copy[key]
result_value = getattr(result, key)
if check_as_dict:
result_value = result.get(key, None)
else:
result_value = getattr(result, key)
print(f'KWARG VALUE:\n{kwarg_value}')
print(f'RESULT VALUE:\n{result_value}')
assert does_kwarg_value_match_result(kwargs_copy[key],
getattr(result, key)) is True
result_value) is True
else:
with pytest.raises(error):
result = cls(**kwargs)
Expand Down Expand Up @@ -569,7 +572,7 @@ def Class__to_untrimmed_dict(cls, kwargs, error):
result = instance._to_untrimmed_dict()


def Class_from_dict(cls, kwargs, error):
def Class_from_dict(cls, kwargs, error, check_as_dict = False):
if kwargs:
as_dict = to_js_dict(deepcopy(kwargs))
else:
Expand Down Expand Up @@ -617,6 +620,8 @@ def Class_from_dict(cls, kwargs, error):
result_value = getattr(instance, 'pattern_options')
elif key == 'type':
result_value = getattr(instance, 'type')
elif check_as_dict:
result_value = instance.get(key, None)
else:
result_value = getattr(instance, key)
print(kwarg_value)
Expand All @@ -627,9 +632,12 @@ def Class_from_dict(cls, kwargs, error):
instance = cls.from_dict(as_dict)


def Class_to_dict(cls, kwargs, error):
def Class_to_dict(cls, kwargs, error, trim_expected = True):
untrimmed_expected = to_js_dict(deepcopy(kwargs))
expected = trim_expected(untrimmed_expected)
if trim_expected:
expected = trim_expected_dict(untrimmed_expected)
else:
expected = untrimmed_expected
check_dicts = True
for key in expected:
if not checkers.is_type(expected[key], (str, int, float, bool, list, dict)):
Expand Down
4 changes: 2 additions & 2 deletions tests/input_files/chart_obj/01-expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,8 +763,8 @@ var someVariableName = Highcharts.chart('some-div-id',
enabled: true,
text: 'Button Label',
theme: {
fill: '#fff',
stroke: '#ccc'
'fill': '#fff',
'stroke': '#ccc'
},
y: 0
},
Expand Down
4 changes: 2 additions & 2 deletions tests/input_files/chart_obj/01-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,8 +765,8 @@
enabled: true,
text: 'Button Label',
theme: {
fill: '#fff',
stroke: '#ccc'
'fill': '#fff',
'stroke': '#ccc'
},
y: 0
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,8 @@ Highcharts.setOptions({
enabled: true,
text: 'Button Label',
theme: {
fill: '#fff',
stroke: '#ccc'
'fill': '#fff',
'stroke': '#ccc'
},
y: 0
},
Expand Down
Loading