Skip to content

Commit bd673ca

Browse files
authored
Merge pull request #88 from highcharts-for-python/develop
PR for v.1.3.4
2 parents ff626c3 + ab19eeb commit bd673ca

File tree

18 files changed

+335
-50
lines changed

18 files changed

+335
-50
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11

2+
Release 1.3.4
3+
=========================================
4+
5+
* **ENHANCEMENT:** Converted `ButtonTheme` into an extensible descendent of `JavaScriptDict` (#86).
6+
7+
---------------------
8+
29
Release 1.3.3
310
=========================================
411

highcharts_core/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.3.3'
1+
__version__ = '1.3.4'

highcharts_core/metaclasses.py

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -796,14 +796,20 @@ def __setitem__(self, key, item):
796796
super().__setitem__(key, item)
797797

798798
@classmethod
799-
def from_dict(cls, as_dict):
799+
def from_dict(cls,
800+
as_dict: dict,
801+
allow_snake_case: bool = True):
800802
"""Construct an instance of the class from a :class:`dict <python:dict>` object.
801803
802804
:param as_dict: A :class:`dict <python:dict>` representation of the object.
803805
:type as_dict: :class:`dict <python:dict>`
804806
807+
:param allow_snake_case: If ``True``, interprets ``snake_case`` keys as equivalent
808+
to ``camelCase`` keys. Defaults to ``True``.
809+
:type allow_snake_case: :class:`bool <python:bool>`
810+
805811
:returns: A Python object representation of ``as_dict``.
806-
:rtype: :class:`JavaScriptDict`
812+
:rtype: :class:`HighchartsMeta`
807813
"""
808814
as_dict = validators.dict(as_dict, allow_empty = True)
809815
if not as_dict:
@@ -1120,9 +1126,98 @@ def _validate_js_literal(cls,
11201126
range = range,
11211127
_break_loop_on_failure = True)
11221128
else:
1123-
raise errors.HighchartsParseError('._validate_js_function() expects '
1129+
raise errors.HighchartsParseError('._validate_js_literal() expects '
11241130
'a str containing a valid '
1125-
'JavaScript function. Could not '
1126-
'find a valid function.')
1131+
'JavaScript literal object. Could '
1132+
'not find a valid JS literal '
1133+
'object.')
11271134

11281135
return parsed, as_str
1136+
1137+
@classmethod
1138+
def from_js_literal(cls,
1139+
as_str_or_file,
1140+
allow_snake_case: bool = True,
1141+
_break_loop_on_failure: bool = False):
1142+
"""Return a Python object representation of a Highcharts JavaScript object
1143+
literal.
1144+
1145+
:param as_str_or_file: The JavaScript object literal, represented either as a
1146+
:class:`str <python:str>` or as a filename which contains the JS object literal.
1147+
:type as_str_or_file: :class:`str <python:str>`
1148+
1149+
:param allow_snake_case: If ``True``, interprets ``snake_case`` keys as equivalent
1150+
to ``camelCase`` keys. Defaults to ``True``.
1151+
:type allow_snake_case: :class:`bool <python:bool>`
1152+
1153+
:param _break_loop_on_failure: If ``True``, will break any looping operations in
1154+
the event of a failure. Otherwise, will attempt to repair the failure. Defaults
1155+
to ``False``.
1156+
:type _break_loop_on_failure: :class:`bool <python:bool>`
1157+
1158+
:returns: A Python object representation of the Highcharts JavaScript object
1159+
literal.
1160+
:rtype: :class:`HighchartsMeta`
1161+
"""
1162+
is_file = checkers.is_file(as_str_or_file)
1163+
if is_file:
1164+
with open(as_str_or_file, 'r') as file_:
1165+
as_str = file_.read()
1166+
else:
1167+
as_str = as_str_or_file
1168+
1169+
parsed, updated_str = cls._validate_js_literal(as_str)
1170+
1171+
as_dict = {}
1172+
if not parsed.body:
1173+
return cls()
1174+
1175+
if len(parsed.body) > 1:
1176+
raise errors.HighchartsCollectionError(f'each JavaScript object literal is '
1177+
f'expected to contain one object. '
1178+
f'However, you attempted to parse '
1179+
f'{len(parsed.body)} objects.')
1180+
1181+
body = parsed.body[0]
1182+
if not checkers.is_type(body, 'VariableDeclaration') and \
1183+
_break_loop_on_failure is False:
1184+
prefixed_str = f'var randomVariable = {as_str}'
1185+
return cls.from_js_literal(prefixed_str,
1186+
_break_loop_on_failure = True)
1187+
elif not checkers.is_type(body, 'VariableDeclaration'):
1188+
raise errors.HighchartsVariableDeclarationError('To parse a JavaScriot '
1189+
'object literal, it is '
1190+
'expected to be either a '
1191+
'variable declaration or a'
1192+
'standalone block statement.'
1193+
'Input received did not '
1194+
'conform.')
1195+
declarations = body.declarations
1196+
if not declarations:
1197+
return cls()
1198+
1199+
if len(declarations) > 1:
1200+
raise errors.HighchartsCollectionError(f'each JavaScript object literal is '
1201+
f'expected to contain one object. '
1202+
f'However, you attempted to parse '
1203+
f'{len(parsed.body)} objects.')
1204+
object_expression = declarations[0].init
1205+
if not checkers.is_type(object_expression, 'ObjectExpression'):
1206+
raise errors.HighchartsParseError(f'Highcharts expects an object literal to '
1207+
f'to be defined as a standard '
1208+
f'ObjectExpression. Received: '
1209+
f'{type(object_expression)}')
1210+
1211+
properties = object_expression.properties
1212+
if not properties:
1213+
return cls()
1214+
1215+
key_value_pairs = [(x[0], x[1]) for x in get_key_value_pairs(properties,
1216+
updated_str)]
1217+
1218+
for pair in key_value_pairs:
1219+
as_dict[pair[0]] = pair[1]
1220+
1221+
return cls.from_dict(as_dict,
1222+
allow_snake_case = allow_snake_case)
1223+

highcharts_core/utility_classes/buttons.py

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,57 @@
2020
from highcharts_core.utility_classes.gradients import Gradient
2121
from highcharts_core.utility_classes.patterns import Pattern
2222
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
23+
from highcharts_core.utility_classes.ast import AttributeObject
24+
from highcharts_core.utility_classes.states import States
2325

2426

25-
class ButtonTheme(HighchartsMeta):
27+
class ButtonTheme(AttributeObject):
2628
"""Settings used to style buttons."""
2729

2830
def __init__(self, **kwargs):
29-
self._fill = None
30-
self._stroke = None
31+
trimmed_kwargs = {x: y for x, y in kwargs.items() if not hasattr(self, x)}
32+
super().__init__(**trimmed_kwargs)
3133

3234
self.fill = kwargs.get('fill', None)
35+
self.padding = kwargs.get('padding', None)
3336
self.stroke = kwargs.get('stroke', None)
37+
self.states = kwargs.get('states', None)
38+
39+
def __setitem__(self, key, item):
40+
validate_key = False
41+
try:
42+
validate_key = key not in self
43+
except AttributeError:
44+
validate_key = True
45+
46+
if validate_key:
47+
try:
48+
key = validators.variable_name(key, allow_empty = False)
49+
except validator_errors.InvalidVariableNameError as error:
50+
if '-' in key:
51+
try:
52+
test_key = key.replace('-', '_')
53+
validators.variable_name(test_key, allow_empty = False)
54+
except validator_errors.InvalidVariableNameError:
55+
raise error
56+
else:
57+
raise error
58+
59+
if self._valid_value_types:
60+
try:
61+
item = validate_types(item,
62+
types = self._valid_value_types,
63+
allow_none = self._allow_empty_value)
64+
except errors.HighchartsValueError as error:
65+
if self._allow_empty_value and not item:
66+
item = None
67+
else:
68+
try:
69+
item = self._valid_value_types(item)
70+
except (TypeError, ValueError, AttributeError):
71+
raise error
72+
73+
super().__setitem__(key, item)
3474

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

4585
@fill.setter
4686
def fill(self, value):
47-
self._fill = utility_functions.validate_color(value)
87+
self['fill'] = utility_functions.validate_color(value)
88+
89+
@property
90+
def padding(self) -> Optional[int | float | Decimal]:
91+
"""Padding for the button. Defaults to `5`.
92+
93+
:rtype: numeric
94+
"""
95+
return self.get('padding', None)
96+
97+
@padding.setter
98+
def padding(self, value):
99+
self['padding'] = validators.numeric(value, allow_empty = True)
100+
101+
@property
102+
def states(self) -> Optional[States]:
103+
"""States to apply to the button. Defaults to :obj:`None <python:None>`.
104+
105+
:rtype: :class:`States <highcharts_core.utility_classes.states.States>` or
106+
:obj:`None <python:None>`
107+
"""
108+
return self.get('states', None)
109+
110+
@states.setter
111+
def states(self, value):
112+
self['states'] = validate_types(value, States)
48113

49114
@property
50115
def stroke(self) -> Optional[str]:
51116
"""The color of the button's stroke. Defaults to ``'none'``.
52117
53118
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
54119
"""
55-
return self._stroke
120+
return self.get('stroke', None)
56121

57122
@stroke.setter
58123
def stroke(self, value):
59-
self._stroke = validators.string(value, allow_empty = True)
124+
self['stroke'] = validators.string(value, allow_empty = True)
60125

61126
@classmethod
62127
def _get_kwargs_from_dict(cls, as_dict):
63128
kwargs = {
64129
'fill': as_dict.get('fill', None),
130+
'padding': as_dict.get('padding', None),
131+
'states': as_dict.get('states', None),
65132
'stroke': as_dict.get('stroke', None)
66133
}
134+
135+
for key in as_dict:
136+
if key not in kwargs:
137+
kwargs[key] = as_dict.get(key, None)
67138

68139
return kwargs
69140

70-
def _to_untrimmed_dict(self, in_cls = None) -> dict:
71-
untrimmed = {
72-
'fill': self.fill,
73-
'stroke': self.stroke
74-
}
75-
76-
return untrimmed
77-
78141

79142
class ButtonConfiguration(HighchartsMeta):
80143
"""Configuration of options that apply to a given button."""

tests/fixtures.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def does_kwarg_value_match_result(kwarg_value, result_value):
255255
return True
256256

257257

258-
def trim_expected(expected):
258+
def trim_expected_dict(expected):
259259
"""Remove keys from ``expected`` or its children that should not be evaluated."""
260260
new_dict = {}
261261
if not isinstance(expected, dict):
@@ -264,13 +264,13 @@ def trim_expected(expected):
264264
if expected[key] is None:
265265
continue
266266
elif isinstance(expected[key], dict):
267-
trimmed_value = trim_expected(expected[key])
267+
trimmed_value = trim_expected_dict(expected[key])
268268
if trimmed_value:
269269
new_dict[key] = trimmed_value
270270
elif checkers.is_iterable(expected[key]):
271271
trimmed_value = []
272272
for item in expected[key]:
273-
trimmed_item = trim_expected(item)
273+
trimmed_item = trim_expected_dict(item)
274274
if trimmed_item:
275275
trimmed_value.append(trimmed_item)
276276

@@ -314,7 +314,7 @@ def compare_js_literals(original, new):
314314
return True
315315

316316

317-
def Class__init__(cls, kwargs, error):
317+
def Class__init__(cls, kwargs, error, check_as_dict = False):
318318
kwargs_copy = deepcopy(kwargs)
319319
if not error:
320320
result = cls(**kwargs)
@@ -365,11 +365,14 @@ def Class__init__(cls, kwargs, error):
365365
else:
366366
print('not margin')
367367
kwarg_value = kwargs_copy[key]
368-
result_value = getattr(result, key)
368+
if check_as_dict:
369+
result_value = result.get(key, None)
370+
else:
371+
result_value = getattr(result, key)
369372
print(f'KWARG VALUE:\n{kwarg_value}')
370373
print(f'RESULT VALUE:\n{result_value}')
371374
assert does_kwarg_value_match_result(kwargs_copy[key],
372-
getattr(result, key)) is True
375+
result_value) is True
373376
else:
374377
with pytest.raises(error):
375378
result = cls(**kwargs)
@@ -569,7 +572,7 @@ def Class__to_untrimmed_dict(cls, kwargs, error):
569572
result = instance._to_untrimmed_dict()
570573

571574

572-
def Class_from_dict(cls, kwargs, error):
575+
def Class_from_dict(cls, kwargs, error, check_as_dict = False):
573576
if kwargs:
574577
as_dict = to_js_dict(deepcopy(kwargs))
575578
else:
@@ -617,6 +620,8 @@ def Class_from_dict(cls, kwargs, error):
617620
result_value = getattr(instance, 'pattern_options')
618621
elif key == 'type':
619622
result_value = getattr(instance, 'type')
623+
elif check_as_dict:
624+
result_value = instance.get(key, None)
620625
else:
621626
result_value = getattr(instance, key)
622627
print(kwarg_value)
@@ -627,9 +632,12 @@ def Class_from_dict(cls, kwargs, error):
627632
instance = cls.from_dict(as_dict)
628633

629634

630-
def Class_to_dict(cls, kwargs, error):
635+
def Class_to_dict(cls, kwargs, error, trim_expected = True):
631636
untrimmed_expected = to_js_dict(deepcopy(kwargs))
632-
expected = trim_expected(untrimmed_expected)
637+
if trim_expected:
638+
expected = trim_expected_dict(untrimmed_expected)
639+
else:
640+
expected = untrimmed_expected
633641
check_dicts = True
634642
for key in expected:
635643
if not checkers.is_type(expected[key], (str, int, float, bool, list, dict)):

tests/input_files/chart_obj/01-expected.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,8 +763,8 @@ var someVariableName = Highcharts.chart('some-div-id',
763763
enabled: true,
764764
text: 'Button Label',
765765
theme: {
766-
fill: '#fff',
767-
stroke: '#ccc'
766+
'fill': '#fff',
767+
'stroke': '#ccc'
768768
},
769769
y: 0
770770
},

tests/input_files/chart_obj/01-input.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -765,8 +765,8 @@
765765
enabled: true,
766766
text: 'Button Label',
767767
theme: {
768-
fill: '#fff',
769-
stroke: '#ccc'
768+
'fill': '#fff',
769+
'stroke': '#ccc'
770770
},
771771
y: 0
772772
},

tests/input_files/global_options/shared_options/01-expected.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,8 +762,8 @@ Highcharts.setOptions({
762762
enabled: true,
763763
text: 'Button Label',
764764
theme: {
765-
fill: '#fff',
766-
stroke: '#ccc'
765+
'fill': '#fff',
766+
'stroke': '#ccc'
767767
},
768768
y: 0
769769
},

0 commit comments

Comments
 (0)