Skip to content

Commit 64b8abb

Browse files
committed
Introduce more flexible strict JSON implementation
The previous version was impossible to maintain across different json versions.
1 parent 26b1b51 commit 64b8abb

File tree

1 file changed

+35
-52
lines changed

1 file changed

+35
-52
lines changed

plotly/utils.py

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
77
"""
88

9-
import json
109
import os.path
1110
import sys
1211
import threading
@@ -32,6 +31,11 @@
3231
except ImportError:
3332
_sage_imported = False
3433

34+
if sys.version[:3] == '2.6':
35+
import simplejson as json
36+
else:
37+
import json
38+
3539

3640
### incase people are using threading, we lock file reads
3741
lock = threading.Lock()
@@ -124,66 +128,45 @@ class PlotlyJSONEncoder(json.JSONEncoder):
124128
version.
125129
126130
"""
131+
def coerce_to_strict(self, const):
132+
"""
133+
This is used to ultimately *encode* into strict JSON, see `encode`
127134
128-
# we want stricter JSON, so convert NaN, Inf, -Inf --> 'null'
129-
nan_str = inf_str = neg_inf_str = 'null'
130-
131-
# uses code from official python json.encoder module. Same licence applies.
132-
def iterencode(self, o, _one_shot=False):
133135
"""
134-
Encode the given object and yield each string
135-
representation as available.
136+
# before python 2.7, 'true', 'false', 'null', were include here.
137+
if const in ('Infinity', '-Infinity', 'NaN'):
138+
return None
139+
else:
140+
return const
136141

137-
For example::
142+
def encode(self, o):
143+
"""
144+
Load and then dump the result using parse_constant kwarg
138145
139-
for chunk in JSONEncoder().iterencode(bigobject):
140-
mysocket.write(chunk)
146+
Note that setting invalid separators will cause a failure at this step.
141147
142148
"""
143-
if self.check_circular:
144-
markers = {}
145-
else:
146-
markers = None
147-
if self.ensure_ascii:
148-
_encoder = json.encoder.encode_basestring_ascii
149-
else:
150-
_encoder = json.encoder.encode_basestring
151-
if self.encoding != 'utf-8':
152-
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
153-
if isinstance(o, str):
154-
o = o.decode(_encoding)
155-
return _orig_encoder(o)
156-
157-
def floatstr(o, allow_nan=self.allow_nan,
158-
_repr=json.encoder.FLOAT_REPR, _inf=json.encoder.INFINITY,
159-
_neginf=-json.encoder.INFINITY):
160-
# Check for specials. Note that this type of test is processor
161-
# and/or platform-specific, so do tests which don't depend on the
162-
# internals.
163-
164-
# *any* two NaNs are not equivalent (even to itself) try:
165-
# float('NaN') == float('NaN')
166-
if o != o:
167-
text = self.nan_str
168-
elif o == _inf:
169-
text = self.inf_str
170-
elif o == _neginf:
171-
text = self.neg_inf_str
172-
else:
173-
return _repr(o)
174149

175-
if not allow_nan:
176-
raise ValueError(
177-
"Out of range float values are not JSON compliant: " +
178-
repr(o))
150+
# this will raise errors in a normal-expected way
151+
encoded_o = super(PlotlyJSONEncoder, self).encode(o)
179152

180-
return text
153+
# now:
154+
# 1. `loads` to switch Infinity, -Infinity, NaN to None
155+
# 2. `dumps` again so you get 'null' instead of extended JSON
156+
try:
157+
new_o = json.loads(encoded_o, parse_constant=self.coerce_to_strict)
158+
except ValueError:
181159

182-
_iterencode = json.encoder._make_iterencode(
183-
markers, self.default, _encoder, self.indent, floatstr,
184-
self.key_separator, self.item_separator, self.sort_keys,
185-
self.skipkeys, _one_shot)
186-
return _iterencode(o, 0)
160+
# invalid separators will fail here. raise a helpful exception
161+
raise ValueError(
162+
"Encoding into strict JSON failed. Did you set the separators "
163+
"valid JSON separators?"
164+
)
165+
else:
166+
return json.dumps(new_o, sort_keys=self.sort_keys,
167+
indent=self.indent,
168+
separators=(self.item_separator,
169+
self.key_separator))
187170

188171
def default(self, obj):
189172
"""

0 commit comments

Comments
 (0)