diff --git a/CHANGES.rst b/CHANGES.rst index ad81651a..b6646adf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,13 @@ +Release 1.4.3 +========================================= + +* **BUGFIX:** Fixed edge case error when deserializing ``ChartOptions`` using ``.from_dict()`` + with a ``dict`` that had been serialized using ``.to_dict()`` which errored on ``.margin`` + and ``.spacing`` (#124). + +-------------------- + Release 1.4.2 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index 4b5bf2c5..241893a2 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.4.2' \ No newline at end of file +__version__ = '1.4.3' \ No newline at end of file diff --git a/highcharts_core/options/chart/__init__.py b/highcharts_core/options/chart/__init__.py index 519cf5a4..7c48bf27 100644 --- a/highcharts_core/options/chart/__init__.py +++ b/highcharts_core/options/chart/__init__.py @@ -592,10 +592,22 @@ def margin(self, value): f'or an iterable of four values. ' f'Received an iterable of {len(value)} ' f'values ({value})') - self.margin_top = value[0] - self.margin_right = value[1] - self.margin_bottom = value[2] - self.margin_left = value[3] + if value[0] == 'null': + self.margin_top = None + else: + self.margin_top = value[0] + if value[1] == 'null': + self.margin_right = None + else: + self.margin_right = value[1] + if value[2] == 'null': + self.margin_bottom = None + else: + self.margin_bottom = value[2] + if value[3] == 'null': + self.margin_left = None + else: + self.margin_left = value[3] else: self.margin_top = value self.margin_right = value @@ -620,7 +632,10 @@ def margin_bottom(self) -> Optional[int | float | Decimal]: @margin_bottom.setter def margin_bottom(self, value): - self._margin_bottom = validators.numeric(value, allow_empty = True) + if value is None or isinstance(value, constants.EnforcedNullType): + self._margin_bottom = None + else: + self._margin_bottom = validators.numeric(value) @property def margin_left(self) -> Optional[int | float | Decimal]: @@ -640,7 +655,10 @@ def margin_left(self) -> Optional[int | float | Decimal]: @margin_left.setter def margin_left(self, value): - self._margin_left = validators.numeric(value, allow_empty = True) + if value is None or isinstance(value, constants.EnforcedNullType): + self._margin_left = None + else: + self._margin_left = validators.numeric(value) @property def margin_right(self) -> Optional[int | float | Decimal]: @@ -660,7 +678,10 @@ def margin_right(self) -> Optional[int | float | Decimal]: @margin_right.setter def margin_right(self, value): - self._margin_right = validators.numeric(value, allow_empty = True) + if value is None or isinstance(value, constants.EnforcedNullType): + self._margin_right = None + else: + self._margin_right = validators.numeric(value) @property def margin_top(self) -> Optional[int | float | Decimal]: @@ -680,7 +701,10 @@ def margin_top(self) -> Optional[int | float | Decimal]: @margin_top.setter def margin_top(self, value): - self._margin_top = validators.numeric(value, allow_empty = True) + if value is None or isinstance(value, constants.EnforcedNullType): + self._margin_top = None + else: + self._margin_top = validators.numeric(value) @property def number_formatter(self) -> Optional[CallbackFunction]: @@ -1119,13 +1143,23 @@ def spacing(self, value): f' or an iterable of four values. ' f'Received an iterable of {len(value)} ' f'values ({value})') - value = [validators.numeric(x) for x in value] - self.spacing_top = value[0] - self.spacing_right = value[1] - self.spacing_bottom = value[2] - self.spacing_left = value[3] + if value[0] == 'null': + self.spacing_top = None + else: + self.spacing_top = value[0] + if value[1] == 'null': + self.spacing_right = None + else: + self.spacing_right = value[1] + if value[2] == 'null': + self.spacing_bottom = None + else: + self.spacing_bottom = value[2] + if value[3] == 'null': + self.spacing_left = None + else: + self.spacing_left = value[3] else: - value = validators.numeric(value, allow_empty = False) self.spacing_top = value self.spacing_right = value self.spacing_bottom = value diff --git a/tests/options/chart/test_chart.py b/tests/options/chart/test_chart.py index bb876ef9..e939f817 100644 --- a/tests/options/chart/test_chart.py +++ b/tests/options/chart/test_chart.py @@ -177,3 +177,166 @@ def test_to_dict(kwargs, error): ]) def test_from_js_literal(input_files, filename, as_file, error): Class_from_js_literal(cls, input_files, filename, as_file, error) + + +@pytest.mark.parametrize('as_dict, as_js_literal, error', [ + ({ + 'marginRight': 124 + }, False, None), + ({ + 'type': 'bar', + 'marginRight': 124, + 'marginTop': 421, + 'marginBottom': 321, + 'marginLeft': 789, + 'scrollablePlotArea': { + 'minHeight': 1000, + 'opacity': 1 + } + }, False, None), + + ({ + 'marginRight': 124 + }, True, None), + ({ + 'type': 'bar', + 'marginRight': 124, + 'marginTop': 421, + 'marginBottom': 321, + 'marginLeft': 789, + 'scrollablePlotArea': { + 'minHeight': 1000, + 'opacity': 1 + } + }, True, None), +]) +def test_bug124_margin_right(as_dict, as_js_literal, error): + if not error: + if not as_js_literal: + result = cls.from_dict(as_dict) + else: + as_str = str(as_dict) + result = cls.from_js_literal(as_str) + assert isinstance(result, cls) is True + if 'marginRight' in as_dict or 'margin_right' in as_dict: + assert result.margin_right == as_dict.get('marginRight', None) + if 'marginTop' in as_dict or 'margin_top' in as_dict: + assert result.margin_top == as_dict.get('marginTop', None) + if 'marginBottom' in as_dict or 'margin_bottom' in as_dict: + assert result.margin_bottom == as_dict.get('marginBottom', None) + if 'marginLeft' in as_dict or 'margin_left' in as_dict: + assert result.margin_left == as_dict.get('marginLeft', None) + else: + with pytest.raises(error): + if not as_js_literal: + result = cls.from_dict(as_dict) + else: + as_str = str(as_dict) + result = cls.from_js_literal(as_str) + + +@pytest.mark.parametrize('as_str, error', [ + ("""{ + marginRight: 124 + }""", None), + ("""{type: 'bar', + marginRight: 124, + marginTop: 421, + marginBottom: 321, + marginLeft: 789, + scrollablePlotArea: { + minHeight: 1000, + opacity: 1 + } + }""", None), + + ("""{ + marginRight: null + }""", None), +]) +def test_bug124_margin_right_from_js_literal(as_str, error): + if not error: + result = cls.from_js_literal(as_str) + assert isinstance(result, cls) is True + if 'marginRight' in as_str or 'margin_right' in as_str: + if 'marginRight: null' not in as_str: + assert result.margin_right is not None + else: + assert result.margin_right is None + if 'marginTop' in as_str or 'margin_top' in as_str: + assert result.margin_top is not None + if 'marginBottom' in as_str or 'margin_bottom' in as_str: + assert result.margin_bottom is not None + if 'marginLeft' in as_str or 'margin_left' in as_str: + assert result.margin_left is not None + else: + with pytest.raises(error): + result = cls.from_js_literal(as_str) + + +@pytest.mark.parametrize('as_dict, error', [ + ({ + 'marginRight': 124 + }, None), + ({ + 'type': 'bar', + 'marginRight': 124, + 'marginTop': 421, + 'marginBottom': 321, + 'marginLeft': 789, + 'scrollablePlotArea': { + 'minHeight': 1000, + 'opacity': 1 + } + }, None), + +]) +def test_bug124_margin_right_to_dict_from_dict(as_dict, error): + if not error: + initial_result = cls.from_dict(as_dict) + as_new_dict = initial_result.to_dict() + result = cls.from_dict(as_new_dict) + assert isinstance(result, cls) is True + assert result.margin_right == initial_result.margin_right + assert result.margin_top == initial_result.margin_top + assert result.margin_bottom == initial_result.margin_bottom + assert result.margin_left == initial_result.margin_left + else: + with pytest.raises(error): + initial_result = cls.from_dict(as_dict) + as_new_dict = initial_result.to_dict() + result = cls.from_dict(as_new_dict) + + +@pytest.mark.parametrize('as_dict, error', [ + ({ + 'spacingRight': 124 + }, None), + ({ + 'type': 'bar', + 'spacingRight': 124, + 'spacingTop': 421, + 'spacingBottom': 321, + 'spacingLeft': 789, + 'scrollablePlotArea': { + 'minHeight': 1000, + 'opacity': 1 + } + }, None), + +]) +def test_bug124_spacing_right_to_dict_from_dict(as_dict, error): + if not error: + initial_result = cls.from_dict(as_dict) + as_new_dict = initial_result.to_dict() + result = cls.from_dict(as_new_dict) + assert isinstance(result, cls) is True + assert result.spacing_right == initial_result.spacing_right + assert result.spacing_top == initial_result.spacing_top + assert result.spacing_bottom == initial_result.spacing_bottom + assert result.spacing_left == initial_result.spacing_left + else: + with pytest.raises(error): + initial_result = cls.from_dict(as_dict) + as_new_dict = initial_result.to_dict() + result = cls.from_dict(as_new_dict)