Skip to content

Commit 2bb8a58

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 * Generates a non-public `FrameEntry` graph obj * Tests this functionality
1 parent 3df1290 commit 2bb8a58

File tree

6 files changed

+193
-41
lines changed

6 files changed

+193
-41
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+
['dict']
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_objs/graph_objs_tools.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ def get_help(object_name, path=(), parent_object_names=(), attribute=None):
3535
def _list_help(object_name, path=(), parent_object_names=()):
3636
"""See get_help()."""
3737
items = graph_reference.ARRAYS[object_name]['items']
38-
items_classes = [graph_reference.string_to_class_name(item)
39-
for item in items]
38+
items_classes = set()
39+
for item in items:
40+
if item in graph_reference.OBJECT_NAME_TO_CLASS_NAME:
41+
items_classes.add(graph_reference.string_to_class_name(item))
42+
else:
43+
# There are no lists objects which can contain list entries.
44+
items_classes.add('dict')
45+
items_classes = list(items_classes)
4046
items_classes.sort()
4147
lines = textwrap.wrap(repr(items_classes), width=LINE_SIZE-TAB_SIZE-1)
4248

plotly/graph_reference.py

+17-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},
@@ -410,12 +410,25 @@ def _patch_objects():
410410
'attribute_paths': layout_attribute_paths,
411411
'additional_attributes': {}}
412412

413-
figure_attributes = {'layout': {'role': 'object'},
414-
'data': {'role': 'object', '_isLinkedToArray': True}}
413+
figure_attributes = {
414+
'layout': {'role': 'object'},
415+
'data': {'role': 'object', '_isLinkedToArray': True},
416+
'frames': {'role': 'object', '_isLinkedToArray': True}
417+
}
415418
OBJECTS['figure'] = {'meta_paths': [],
416419
'attribute_paths': [],
417420
'additional_attributes': figure_attributes}
418421

422+
frame_entry_attributes = {
423+
'data': {'role': 'object', '_isLinkedToArray': True},
424+
'group': {'role': 'info'},
425+
'layout': {'role': 'object'},
426+
'name': {'role': 'info'}
427+
}
428+
OBJECTS['frame_entry'] = {'meta_paths': [],
429+
'attribute_paths': [],
430+
'additional_attributes': frame_entry_attributes}
431+
419432

420433
def _get_arrays():
421434
"""Very few arrays, but this dict is the complement of OBJECTS."""
@@ -446,6 +459,7 @@ def _get_arrays():
446459
def _patch_arrays():
447460
"""Adds information on our eventual Data array."""
448461
ARRAYS['data'] = {'meta_paths': [('traces', )], 'items': list(TRACE_NAMES)}
462+
ARRAYS['frames'] = {'meta_paths': [], 'items': ['frame_entry']}
449463

450464

451465
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,45 @@
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 = [
14+
{},
15+
{'data': []},
16+
'foo',
17+
{'data': [], 'group': 'baz', 'layout': {}, 'name': 'hoopla'}
18+
]
19+
20+
Frames(native_frames)
21+
Frames()
22+
23+
def test_string_frame(self):
24+
frames = Frames()
25+
frames.append({'group': 'baz', 'data': []})
26+
frames.append('foobar')
27+
self.assertEqual(frames[1], 'foobar')
28+
self.assertEqual(frames.to_string(),
29+
"Frames([\n"
30+
" dict(\n"
31+
" data=Data(),\n"
32+
" group='baz'\n"
33+
" ),\n"
34+
" 'foobar'\n"
35+
"])")
36+
37+
def test_non_string_frame(self):
38+
frames = Frames()
39+
frames.append({})
40+
41+
with self.assertRaises(exceptions.PlotlyListEntryError):
42+
frames.append([])
43+
44+
with self.assertRaises(exceptions.PlotlyListEntryError):
45+
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)