Skip to content

Commit 851b21a

Browse files
committed
Improve frames support in graph_objs.py.
This does the following: * Allow object-or-string in frames array * Refuse `frames` in frame object if nested * Loosely associate a frames entry with `Figure` * Tests this functionality
1 parent 3df1290 commit 851b21a

File tree

5 files changed

+173
-39
lines changed

5 files changed

+173
-39
lines changed

plotly/graph_objs/graph_objs.py

+42-20
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ def create(object_name, *args, **kwargs):
787787

788788
# We patch Figure and Data, so they actually require the subclass.
789789
class_name = graph_reference.OBJECT_NAME_TO_CLASS_NAME.get(object_name)
790-
if class_name in ['Figure', 'Data']:
790+
if class_name in ['Figure', 'Data', 'Frames']:
791791
return globals()[class_name](*args, **kwargs)
792792
else:
793793
kwargs['_name'] = object_name
@@ -1097,7 +1097,7 @@ class Figure(PlotlyDict):
10971097
"""
10981098
Valid attributes for 'figure' at path [] under parents ():
10991099
1100-
['data', 'layout']
1100+
['data', 'frames', 'layout']
11011101
11021102
Run `<figure-object>.help('attribute')` on any of the above.
11031103
'<figure-object>' is the object at []
@@ -1108,22 +1108,7 @@ class Figure(PlotlyDict):
11081108
def __init__(self, *args, **kwargs):
11091109
super(Figure, self).__init__(*args, **kwargs)
11101110
if 'data' not in self:
1111-
self.data = GraphObjectFactory.create('data', _parent=self,
1112-
_parent_key='data')
1113-
1114-
# TODO better integrate frames into Figure - #604
1115-
def __setitem__(self, key, value, **kwargs):
1116-
if key == 'frames':
1117-
super(PlotlyDict, self).__setitem__(key, value)
1118-
else:
1119-
super(Figure, self).__setitem__(key, value, **kwargs)
1120-
1121-
def _get_valid_attributes(self):
1122-
super(Figure, self)._get_valid_attributes()
1123-
# TODO better integrate frames into Figure - #604
1124-
if 'frames' not in self._valid_attributes:
1125-
self._valid_attributes.add('frames')
1126-
return self._valid_attributes
1111+
self.data = Data(_parent=self, _parent_key='data')
11271112

11281113
def get_data(self, flatten=False):
11291114
"""
@@ -1241,8 +1226,45 @@ class Font(PlotlyDict):
12411226
_name = 'font'
12421227

12431228

1244-
class Frames(dict):
1245-
pass
1229+
class Frames(PlotlyList):
1230+
"""
1231+
Valid items for 'frames' at path [] under parents ():
1232+
['Figure']
1233+
1234+
"""
1235+
_name = 'frames'
1236+
1237+
def _value_to_graph_object(self, index, value, _raise=True):
1238+
if isinstance(value, six.string_types):
1239+
return value
1240+
return super(Frames, self)._value_to_graph_object(index, value,
1241+
_raise=_raise)
1242+
1243+
def to_string(self, level=0, indent=4, eol='\n',
1244+
pretty=True, max_chars=80):
1245+
"""Get formatted string by calling `to_string` on children items."""
1246+
if not len(self):
1247+
return "{name}()".format(name=self._get_class_name())
1248+
string = "{name}([{eol}{indent}".format(
1249+
name=self._get_class_name(),
1250+
eol=eol,
1251+
indent=' ' * indent * (level + 1))
1252+
for index, entry in enumerate(self):
1253+
if isinstance(entry, six.string_types):
1254+
string += repr(entry)
1255+
else:
1256+
string += entry.to_string(level=level+1,
1257+
indent=indent,
1258+
eol=eol,
1259+
pretty=pretty,
1260+
max_chars=max_chars)
1261+
if index < len(self) - 1:
1262+
string += ",{eol}{indent}".format(
1263+
eol=eol,
1264+
indent=' ' * indent * (level + 1))
1265+
string += (
1266+
"{eol}{indent}])").format(eol=eol, indent=' ' * indent * level)
1267+
return string
12461268

12471269

12481270
class Heatmap(PlotlyDict):

plotly/graph_reference.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
'ErrorZ': {'object_name': 'error_z', 'base_type': dict},
3434
'Figure': {'object_name': 'figure', 'base_type': dict},
3535
'Font': {'object_name': 'font', 'base_type': dict},
36-
'Frames': {'object_name': 'frames', 'base_type': dict},
36+
'Frames': {'object_name': 'frames', 'base_type': list},
3737
'Heatmap': {'object_name': 'heatmap', 'base_type': dict},
3838
'Histogram': {'object_name': 'histogram', 'base_type': dict},
3939
'Histogram2d': {'object_name': 'histogram2d', 'base_type': dict},
@@ -168,6 +168,10 @@ def get_valid_attributes(object_name, parent_object_names=()):
168168
if key not in GRAPH_REFERENCE['defs']['metaKeys']:
169169
valid_attributes.add(key)
170170

171+
if object_name == 'figure' and parent_object_names:
172+
# A frame cannot itself contain a frames array.
173+
valid_attributes.remove('frames')
174+
171175
return valid_attributes
172176

173177

@@ -410,8 +414,11 @@ def _patch_objects():
410414
'attribute_paths': layout_attribute_paths,
411415
'additional_attributes': {}}
412416

413-
figure_attributes = {'layout': {'role': 'object'},
414-
'data': {'role': 'object', '_isLinkedToArray': True}}
417+
figure_attributes = {
418+
'layout': {'role': 'object'},
419+
'data': {'role': 'object', '_isLinkedToArray': True},
420+
'frames': {'role': 'object', '_isLinkedToArray': True}
421+
}
415422
OBJECTS['figure'] = {'meta_paths': [],
416423
'attribute_paths': [],
417424
'additional_attributes': figure_attributes}
@@ -446,6 +453,7 @@ def _get_arrays():
446453
def _patch_arrays():
447454
"""Adds information on our eventual Data array."""
448455
ARRAYS['data'] = {'meta_paths': [('traces', )], 'items': list(TRACE_NAMES)}
456+
ARRAYS['frames'] = {'meta_paths': [], 'items': ['figure']}
449457

450458

451459
def _get_classes():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import absolute_import
2+
3+
from unittest import TestCase
4+
5+
from plotly import exceptions
6+
from plotly.graph_objs import Figure
7+
8+
9+
class FigureTest(TestCase):
10+
11+
def test_instantiation(self):
12+
13+
native_figure = {
14+
'data': [],
15+
'layout': {},
16+
'frames': []
17+
}
18+
19+
Figure(native_figure)
20+
Figure()
21+
22+
def test_access_top_level(self):
23+
24+
# Figure is special, we define top-level objects that always exist.
25+
26+
self.assertEqual(Figure().data, [])
27+
self.assertEqual(Figure().layout, {})
28+
self.assertEqual(Figure().frames, [])
29+
30+
def test_nested_frames(self):
31+
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
32+
Figure({'frames': [{'frames': []}]})
33+
34+
figure = Figure()
35+
figure.frames = [{}]
36+
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
37+
figure.frames[0].frames = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import absolute_import
2+
3+
from unittest import TestCase
4+
5+
from plotly import exceptions
6+
from plotly.graph_objs import Frames
7+
8+
9+
class FramesTest(TestCase):
10+
11+
def test_instantiation(self):
12+
13+
native_frames = [{}, {'data': []}, 'foo', {'data': [], 'layout': {}}]
14+
15+
Frames(native_frames)
16+
Frames()
17+
18+
def test_string_frame(self):
19+
frames = Frames()
20+
frames.append({})
21+
frames.append('foobar')
22+
self.assertEqual(frames[1], 'foobar')
23+
self.assertEqual(frames.to_string(),
24+
"Frames([\n"
25+
" Figure(\n"
26+
" data=Data()\n"
27+
" ),\n"
28+
" 'foobar'\n"
29+
"])")
30+
31+
def test_non_string_frame(self):
32+
frames = Frames()
33+
frames.append({})
34+
35+
with self.assertRaises(exceptions.PlotlyListEntryError):
36+
frames.append([])
37+
38+
with self.assertRaises(exceptions.PlotlyListEntryError):
39+
frames.append(0)

update_graph_objs.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import print_function
2+
13
from plotly.graph_objs import graph_objs_tools
24
from plotly.graph_reference import ARRAYS, CLASSES
35

@@ -35,22 +37,7 @@ def print_figure_patch(f):
3537
def __init__(self, *args, **kwargs):
3638
super(Figure, self).__init__(*args, **kwargs)
3739
if 'data' not in self:
38-
self.data = GraphObjectFactory.create('data', _parent=self,
39-
_parent_key='data')
40-
41-
# TODO better integrate frames into Figure - #604
42-
def __setitem__(self, key, value, **kwargs):
43-
if key == 'frames':
44-
super(PlotlyDict, self).__setitem__(key, value)
45-
else:
46-
super(Figure, self).__setitem__(key, value, **kwargs)
47-
48-
def _get_valid_attributes(self):
49-
super(Figure, self)._get_valid_attributes()
50-
# TODO better integrate frames into Figure - #604
51-
if 'frames' not in self._valid_attributes:
52-
self._valid_attributes.add('frames')
53-
return self._valid_attributes
40+
self.data = Data(_parent=self, _parent_key='data')
5441
5542
def get_data(self, flatten=False):
5643
"""
@@ -221,6 +208,45 @@ def get_data(self, flatten=False):
221208
)
222209

223210

211+
def print_frames_patch(f):
212+
"""Print a patch to our Frames object into the given open file."""
213+
print(
214+
'''
215+
def _value_to_graph_object(self, index, value, _raise=True):
216+
if isinstance(value, six.string_types):
217+
return value
218+
return super(Frames, self)._value_to_graph_object(index, value,
219+
_raise=_raise)
220+
221+
def to_string(self, level=0, indent=4, eol='\\n',
222+
pretty=True, max_chars=80):
223+
"""Get formatted string by calling `to_string` on children items."""
224+
if not len(self):
225+
return "{name}()".format(name=self._get_class_name())
226+
string = "{name}([{eol}{indent}".format(
227+
name=self._get_class_name(),
228+
eol=eol,
229+
indent=' ' * indent * (level + 1))
230+
for index, entry in enumerate(self):
231+
if isinstance(entry, six.string_types):
232+
string += repr(entry)
233+
else:
234+
string += entry.to_string(level=level+1,
235+
indent=indent,
236+
eol=eol,
237+
pretty=pretty,
238+
max_chars=max_chars)
239+
if index < len(self) - 1:
240+
string += ",{eol}{indent}".format(
241+
eol=eol,
242+
indent=' ' * indent * (level + 1))
243+
string += (
244+
"{eol}{indent}])").format(eol=eol, indent=' ' * indent * level)
245+
return string
246+
''', file=f, end=''
247+
)
248+
249+
224250
def print_class(name, f):
225251
class_dict = CLASSES[name]
226252
print('\n', file=f)
@@ -250,6 +276,8 @@ def print_class(name, f):
250276
print_figure_patch(f)
251277
elif name == 'Data':
252278
print_data_patch(f)
279+
elif name == 'Frames':
280+
print_frames_patch(f)
253281

254282
copied_lines = get_non_generated_file_lines()
255283
with open('./plotly/graph_objs/graph_objs.py', 'w') as graph_objs_file:

0 commit comments

Comments
 (0)