From f5df1331cb69dfed8ca4ad68ed80f47054df631b Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Tue, 10 Mar 2015 00:19:19 -0700 Subject: [PATCH 1/4] Override `JSONEncoder.iterencode` We do this to handle NaN, Inf, and -Inf --- plotly/utils.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/plotly/utils.py b/plotly/utils.py index 216f0077b20..e948a07507f 100644 --- a/plotly/utils.py +++ b/plotly/utils.py @@ -119,8 +119,72 @@ class PlotlyJSONEncoder(json.JSONEncoder): See PlotlyJSONEncoder.default for more implementation information. + Additionally, this encoder overrides nan functionality so that 'Inf', + 'NaN' and '-Inf' encode to 'null'. Which is stricter JSON than the Python + version. + """ + # we want stricter JSON, so convert NaN, Inf, -Inf --> 'null' + nan_str = inf_str = neg_inf_str = 'null' + + # uses code from official python json.encoder module. Same licence applies. + def iterencode(self, o, _one_shot=False): + """ + Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = json.encoder.encode_basestring_ascii + else: + _encoder = json.encoder.encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, + _repr=json.encoder.FLOAT_REPR, _inf=json.encoder.INFINITY, + _neginf=-json.encoder.INFINITY): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on the + # internals. + + # *any* two NaNs are not equivalent (even to itself) try: + # float('NaN') == float('NaN') + if o != o: + text = self.nan_str + elif o == _inf: + text = self.inf_str + elif o == _neginf: + text = self.neg_inf_str + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + _iterencode = json.encoder._make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + def default(self, obj): """ Accept an object (of unknown type) and try to encode with priority: From c54f643f7a8a452f00958bd02d19e956cef02a68 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Wed, 11 Mar 2015 01:29:37 -0700 Subject: [PATCH 2/4] =?UTF-8?q?Expect=20Infinity/NaN=20=E2=80=94>=20null?= =?UTF-8?q?=20in=20tests=20now.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plotly/tests/test_optional/test_utils/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plotly/tests/test_optional/test_utils/test_utils.py b/plotly/tests/test_optional/test_utils/test_utils.py index dc54e2f1dbd..a1450913860 100644 --- a/plotly/tests/test_optional/test_utils/test_utils.py +++ b/plotly/tests/test_optional/test_utils/test_utils.py @@ -173,7 +173,7 @@ def test_column_json_encoding(): '"2014-01-05 01:01:01", ' '"2014-01-05 01:01:01.000001"], ' '"name": "col 2"}, ' - '{"data": [1, 2, 3, NaN, NaN, Infinity, ' + '{"data": [1, 2, 3, null, null, null, ' '"2014-01-05"], "name": "col 3"}]' == json_columns) @@ -188,7 +188,7 @@ def test_figure_json_encoding(): js2 = json.dumps(s2, cls=utils.PlotlyJSONEncoder, sort_keys=True) assert(js1 == '{"type": "scatter3d", "x": [1, 2, 3], ' - '"y": [1, 2, 3, NaN, NaN, Infinity, "2014-01-05"], ' + '"y": [1, 2, 3, null, null, null, "2014-01-05"], ' '"z": [1, "A", "2014-01-05", ' '"2014-01-05 01:01:01", "2014-01-05 01:01:01.000001"]}') assert(js2 == '{"type": "scatter", "x": [1, 2, 3]}') @@ -219,7 +219,7 @@ def test_datetime_json_encoding(): def test_pandas_json_encoding(): j1 = json.dumps(df['col 1'], cls=utils.PlotlyJSONEncoder) - assert(j1 == '[1, 2, 3, "2014-01-05", null, NaN, Infinity]') + assert(j1 == '[1, 2, 3, "2014-01-05", null, null, null]') # Test that data wasn't mutated assert_series_equal(df['col 1'], @@ -249,7 +249,7 @@ def test_numpy_masked_json_encoding(): l = [1, 2, np.ma.core.masked] j1 = json.dumps(l, cls=utils.PlotlyJSONEncoder) print j1 - assert(j1 == '[1, 2, NaN]') + assert(j1 == '[1, 2, null]') assert(set(l) == set([1, 2, np.ma.core.masked])) @@ -276,7 +276,7 @@ def test_masked_constants_example(): jy = json.dumps(renderer.plotly_fig['data'][1]['y'], cls=utils.PlotlyJSONEncoder) assert(jy == '[-398.11793026999999, -398.11792966000002, ' - '-398.11786308000001, NaN]') + '-398.11786308000001, null]') def test_numpy_dates(): From d4599294aaf1ee5cd706d20657f7e3842547150b Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Wed, 11 Mar 2015 01:36:39 -0700 Subject: [PATCH 3/4] Add a core_test for this functionality. --- plotly/tests/test_core/test_utils/__init__.py | 3 +++ plotly/tests/test_core/test_utils/test_utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 plotly/tests/test_core/test_utils/__init__.py create mode 100644 plotly/tests/test_core/test_utils/test_utils.py diff --git a/plotly/tests/test_core/test_utils/__init__.py b/plotly/tests/test_core/test_utils/__init__.py new file mode 100644 index 00000000000..dbd5b79d852 --- /dev/null +++ b/plotly/tests/test_core/test_utils/__init__.py @@ -0,0 +1,3 @@ +def setup_package(): + import warnings + warnings.filterwarnings('ignore') diff --git a/plotly/tests/test_core/test_utils/test_utils.py b/plotly/tests/test_core/test_utils/test_utils.py new file mode 100644 index 00000000000..5f0a62d88ed --- /dev/null +++ b/plotly/tests/test_core/test_utils/test_utils.py @@ -0,0 +1,13 @@ +import json +from unittest import TestCase + +from plotly.utils import PlotlyJSONEncoder + + +class TestJSONEncoder(TestCase): + + def test_nan_to_null(self): + array = [1, float('NaN'), float('Inf'), float('-Inf'), 'platypus'] + result = json.dumps(array, cls=PlotlyJSONEncoder) + expected_result = '[1, null, null, null, "platypus"]' + self.assertEqual(result, expected_result) From 9af744b9273c1b83aa22912baa2b2fb0e2af1b79 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Wed, 11 Mar 2015 01:40:10 -0700 Subject: [PATCH 4/4] =?UTF-8?q?version=20bump=20=E2=80=94>=201.6.11=20(NaN?= =?UTF-8?q?/Inf=20=E2=80=94>=20null=20in=20JSON)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python uses extended JSON which allows for NaN, Infinity, -Infinity. This means that a `JSON.parse()` on one of our encoded JSON strings will fail. This is no longer the case. --- plotly/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/version.py b/plotly/version.py index 970bb6f45e7..9a5e84e0a7a 100644 --- a/plotly/version.py +++ b/plotly/version.py @@ -1 +1 @@ -__version__ = '1.6.10' +__version__ = '1.6.11'