Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0bd4dc3

Browse files
committed
Refactor PlotlyJSONEncoder. (see below)
Detiails: - renamed *classy* names to *methody* names - made methods `static` - added docstrings to all the things - removed some replication of code - modified the `sage` import to be more explicit - some reordering of the encoders (see TODO) - let `json.JSONEncoder.default` throw `TypeError`
1 parent ac28f7e commit 0bd4dc3

File tree

1 file changed

+105
-67
lines changed

1 file changed

+105
-67
lines changed

plotly/utils.py

Lines changed: 105 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
_pandas_imported = False
2727

2828
try:
29-
from sage.all import RR, ZZ
29+
import sage.all
3030
_sage_imported = True
3131
except ImportError:
3232
_sage_imported = False
@@ -91,109 +91,147 @@ def ensure_dir_exists(directory):
9191
os.makedirs(directory)
9292

9393

94+
def iso_to_plotly_time_string(iso_string):
95+
"""Remove timezone info and replace 'T' delimeter with ' ' (ws)."""
96+
no_tz_string = '-'.join(iso_string.split('-')[:3])
97+
if no_tz_string.endswith('T00:00:00'):
98+
return no_tz_string.replace('T00:00:00', '')
99+
else:
100+
return no_tz_string.replace('T', ' ')
101+
102+
94103
### Custom JSON encoders ###
95104
class NotEncodable(Exception):
96105
pass
97106

98107

99-
def numpyJSONEncoder(self, obj):
100-
if not _numpy_imported:
101-
raise NotEncodable
102108
class PlotlyJSONEncoder(json.JSONEncoder):
109+
"""
110+
Meant to be passed as the `cls` kwarg to json.dumps(obj, cls=..)
103111
104-
if obj is numpy.ma.core.masked:
105-
return float('nan')
112+
See PlotlyJSONEncoder.default for more implementation information.
106113
107-
if type(obj).__module__.split('.')[0] == numpy.__name__:
108-
return obj.tolist()
114+
"""
115+
@staticmethod
116+
def encode_as_plotly(obj):
117+
"""Attempt to use a builtin `ploty_to_json` method."""
118+
try:
119+
return obj.to_plotly_json()
120+
except AttributeError:
121+
raise NotEncodable
109122

123+
@staticmethod
124+
def encode_as_list(obj):
125+
"""Attempt to use `tolist` method to convert to normal Python list."""
126+
if hasattr(obj, 'tolist'):
127+
return obj.tolist()
110128
else:
111129
raise NotEncodable
112130

113-
def datetimeJSONEncoder(self, obj):
114-
"""
115-
if datetime or iterable of datetimes,
116-
convert to a string that plotly understands
117-
format as %Y-%m-%d %H:%M:%S.%f,
118-
%Y-%m-%d %H:%M:%S, or
119-
%Y-%m-%d
120-
depending on what non-zero resolution was provided
121-
"""
122-
123-
if _pandas_imported and obj is pandas.NaT:
124-
return None
125-
131+
@staticmethod
132+
def encode_as_sage(obj):
133+
"""Attempt to convert sage.all.RR to floats and sage.all.ZZ to ints"""
134+
if not _sage_imported:
135+
raise NotEncodable
126136

127-
if isinstance(obj, datetime.datetime):
128-
if obj.microsecond:
129-
return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
130-
elif any((obj.second, obj.minute, obj.hour)):
131-
return obj.strftime('%Y-%m-%d %H:%M:%S')
132-
else:
133-
return obj.strftime('%Y-%m-%d')
134-
elif isinstance(obj, datetime.date):
135-
return obj.strftime('%Y-%m-%d')
137+
if obj in sage.all.RR:
138+
return float(obj)
139+
elif obj in sage.all.ZZ:
140+
return int(obj)
136141
else:
137142
raise NotEncodable
138143

139-
def pandasJSONEncoder(self, obj):
144+
@staticmethod
145+
def encode_as_pandas(obj):
146+
"""Attempt to convert pandas.NaT"""
140147
if not _pandas_imported:
141148
raise NotEncodable
142149

143-
if isinstance(obj, pandas.Series):
144-
serializable_list = []
145-
for li in list(obj):
146-
try:
147-
json.dumps(li)
148-
serializable_list.append(li)
149-
except TypeError:
150-
serializable_list.append(self.default(li))
151-
152-
return serializable_list
153-
elif isinstance(obj, pandas.Index):
154-
return obj.tolist()
155-
elif obj is pandas.NaT:
150+
if obj is pandas.NaT:
156151
return None
157152
else:
158153
raise NotEncodable
159154

160-
def sageJSONEncoder(self, obj):
161-
if not _sage_imported:
155+
@staticmethod
156+
def encode_as_numpy(obj):
157+
"""Attempt to convert numpy.ma.core.masked"""
158+
if not _numpy_imported:
162159
raise NotEncodable
163160

164-
if obj in RR:
165-
return float(obj)
166-
elif obj in ZZ:
167-
return int(obj)
161+
if obj is numpy.ma.core.masked:
162+
return float('nan')
168163
else:
169164
raise NotEncodable
170165

171-
def builtinJSONEncoder(self, obj):
172-
'''
173-
Provide an API for folks to write their own
174-
JSON encoders for their objects that they wanna
175-
send to Plotly: this is an object's `to_plotly_json` method
166+
@staticmethod
167+
def encode_as_datetime(obj):
168+
"""Attempt to convert to utc-iso time string using datetime methods."""
169+
try:
170+
if obj.utcoffset():
171+
obj = obj - obj.utcoffset()
172+
time_string = obj.isoformat()
173+
except AttributeError:
174+
raise NotEncodable
175+
else:
176+
return iso_to_plotly_time_string(time_string)
176177

177-
Used for grid_objs.Column objects.
178-
'''
178+
@staticmethod
179+
def encode_as_date(obj):
180+
"""Attempt to convert to utc-iso time string using date methods."""
179181
try:
180-
return obj.to_plotly_json()
182+
time_string = obj.isoformat()
181183
except AttributeError:
182184
raise NotEncodable
185+
else:
186+
return iso_to_plotly_time_string(time_string)
183187

184188
def default(self, obj):
185-
encoders = (self.builtinJSONEncoder,
186-
self.datetimeJSONEncoder,
187-
self.numpyJSONEncoder,
188-
self.pandasJSONEncoder,
189-
self.sageJSONEncoder)
190-
for encoder in encoders:
189+
"""
190+
Accept an object (of unknown type) and try to encode with priority:
191+
1. builtin: user-defined objects
192+
2. sage: sage math cloud
193+
3. pandas: dataframes/series
194+
4. numpy: ndarrays
195+
5. datetime: time/datetime objects
196+
197+
Each method throws a NotEncoded exception if it fails.
198+
199+
The default method will only get hit if the object is not a type that
200+
is naturally encoded by json:
201+
202+
Normal objects:
203+
dict object
204+
list, tuple array
205+
str, unicode string
206+
int, long, float number
207+
True true
208+
False false
209+
None null
210+
211+
Extended objects:
212+
float('nan') 'NaN'
213+
float('infinity') 'Infinity'
214+
float('-infinity') '-Infinity'
215+
216+
Therefore, we only anticipate either unknown iterables or values here.
217+
218+
"""
219+
# TODO: The ordering if these methods is *very* important. Is this OK?
220+
encoding_methods = (
221+
self.encode_as_plotly,
222+
self.encode_as_sage,
223+
self.encode_as_numpy,
224+
self.encode_as_pandas,
225+
self.encode_as_datetime,
226+
self.encode_as_date,
227+
self.encode_as_list # because some values have `tolist` do last.
228+
)
229+
for encoding_method in encoding_methods:
191230
try:
192-
return encoder(obj)
231+
return encoding_method(obj)
193232
except NotEncodable:
194233
pass
195-
196-
raise TypeError(repr(obj) + " is not JSON serializable")
234+
return json.JSONEncoder.default(self, obj)
197235

198236

199237
### unicode stuff ###

0 commit comments

Comments
 (0)