diff --git a/packages/python/plotly/_plotly_utils/basevalidators.py b/packages/python/plotly/_plotly_utils/basevalidators.py index d4bd8451cef..7e4bdc7bed0 100644 --- a/packages/python/plotly/_plotly_utils/basevalidators.py +++ b/packages/python/plotly/_plotly_utils/basevalidators.py @@ -1033,9 +1033,9 @@ class ColorValidator(BaseValidator): ] }, """ - re_hex = re.compile('#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})') - re_rgb_etc = re.compile('(rgb|hsl|hsv)a?\([\d.]+%?(,[\d.]+%?){2,3}\)') - re_ddk = re.compile('var\(\-\-.*\)') + re_hex = re.compile(r'#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})') + re_rgb_etc = re.compile(r'(rgb|hsl|hsv)a?\([\d.]+%?(,[\d.]+%?){2,3}\)') + re_ddk = re.compile(r'var\(\-\-.*\)') named_colors = [ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", @@ -1456,9 +1456,9 @@ def __init__(self, plotly_name, self.base = dflt else: # e.g. regex == '/^y([2-9]|[1-9][0-9]+)?$/' - self.base = re.match('/\^(\w+)', regex).group(1) + self.base = re.match(r'/\^(\w+)', regex).group(1) - self.regex = self.base + "(\d*)" + self.regex = self.base + r"(\d*)" def description(self): @@ -2268,6 +2268,8 @@ def description(self): that may be specified as: - A list or tuple of trace instances (e.g. [Scatter(...), Bar(...)]) + - A single trace instance + (e.g. Scatter(...), Bar(...), etc.) - A list or tuple of dicts of string/value properties where: - The 'type' property specifies the trace type {trace_types} @@ -2302,7 +2304,10 @@ def validate_coerce(self, v, skip_invalid=False): if v is None: v = [] - elif isinstance(v, (list, tuple)): + else: + if not isinstance(v, (list, tuple)): + v = [v] + trace_classes = tuple(self.class_map.values()) res = [] @@ -2355,12 +2360,6 @@ def validate_coerce(self, v, skip_invalid=False): for trace in v: trace.uid = str(uuid.uuid4()) - else: - if skip_invalid: - v = [] - else: - self.raise_invalid_val(v) - return v diff --git a/packages/python/plotly/_plotly_utils/tests/validators/test_basetraces_validator.py b/packages/python/plotly/_plotly_utils/tests/validators/test_basetraces_validator.py index 67c6be95b92..04cef14cb6e 100644 --- a/packages/python/plotly/_plotly_utils/tests/validators/test_basetraces_validator.py +++ b/packages/python/plotly/_plotly_utils/tests/validators/test_basetraces_validator.py @@ -84,7 +84,7 @@ def test_rejection_type(validator): with pytest.raises(ValueError) as validation_failure: validator.validate_coerce(val) - assert "Invalid value" in str(validation_failure.value) + assert "Invalid element(s)" in str(validation_failure.value) def test_rejection_element_type(validator): diff --git a/packages/python/plotly/_plotly_utils/tests/validators/test_imageuri_validator.py b/packages/python/plotly/_plotly_utils/tests/validators/test_imageuri_validator.py index ff22cbaf1d2..7264cf69d23 100644 --- a/packages/python/plotly/_plotly_utils/tests/validators/test_imageuri_validator.py +++ b/packages/python/plotly/_plotly_utils/tests/validators/test_imageuri_validator.py @@ -1,8 +1,8 @@ import base64 - +import os import pytest from _plotly_utils.basevalidators import ImageUriValidator -import numpy as np + from PIL import Image @@ -28,7 +28,9 @@ def test_validator_acceptance(val, validator): def test_validator_coercion_PIL(validator): # Single pixel black png (http://png-pixel.com/) - img_path = '_plotly_utils/tests/resources/1x1-black.png' + tests_dir = os.path.dirname(os.path.dirname(__file__)) + img_path = os.path.join(tests_dir, 'resources', '1x1-black.png') + with open(img_path, 'rb') as f: hex_bytes = base64.b64encode(f.read()).decode('ascii') expected_uri = 'data:image/png;base64,' + hex_bytes diff --git a/packages/python/plotly/plotly/basedatatypes.py b/packages/python/plotly/plotly/basedatatypes.py index 1d631763ea0..0a09447b239 100644 --- a/packages/python/plotly/plotly/basedatatypes.py +++ b/packages/python/plotly/plotly/basedatatypes.py @@ -498,6 +498,37 @@ def update(self, dict1=None, **kwargs): self[k] = v return self + def pop(self, key, *args): + """ + Remove the value associated with the specified key and return it + + Parameters + ---------- + key: str + Property name + dflt + The default value to return if key was not found in figure + + Returns + ------- + value + The removed value that was previously associated with key + + Raises + ------ + KeyError + If key is not in object and no dflt argument specified + """ + # Handle default + if key not in self and args: + return args[0] + elif key in self: + val = self[key] + self[key] = None + return val + else: + raise KeyError(key) + # Data # ---- @property @@ -520,6 +551,10 @@ def data(self, new_data): 'a list or tuple that contains a permutation of a ' 'subset of itself.\n') + # ### Treat None as empty ### + if new_data is None: + new_data = () + # ### Check valid input type ### if not isinstance(new_data, (list, tuple)): err_msg = (err_header + ' Received value with type {typ}' @@ -859,6 +894,29 @@ def update_traces( trace.update(patch, **kwargs) return self + def update_layout(self, dict1=None, **kwargs): + """ + Update the properties of the figure's layout with a dict and/or with + keyword arguments. + + This recursively updates the structure of the original + layout with the values in the input dict / keyword arguments. + + Parameters + ---------- + dict1 : dict + Dictionary of properties to be updated + kwargs : + Keyword/value pair of properties to be updated + + Returns + ------- + BaseFigure + The Figure object that the update_layout method was called on + """ + self.layout.update(dict1, **kwargs) + return self + def _select_layout_subplots_by_prefix( self, prefix, @@ -3480,6 +3538,37 @@ def update(self, dict1=None, **kwargs): return self + def pop(self, key, *args): + """ + Remove the value associated with the specified key and return it + + Parameters + ---------- + key: str + Property name + dflt + The default value to return if key was not found in object + + Returns + ------- + value + The removed value that was previously associated with key + + Raises + ------ + KeyError + If key is not in object and no dflt argument specified + """ + # Handle default + if key not in self and args: + return args[0] + elif key in self: + val = self[key] + self[key] = None + return val + else: + raise KeyError(key) + @property def _in_batch_mode(self): """ diff --git a/packages/python/plotly/plotly/graph_objects.py b/packages/python/plotly/plotly/graph_objects.py new file mode 100644 index 00000000000..406bd66fd8d --- /dev/null +++ b/packages/python/plotly/plotly/graph_objects.py @@ -0,0 +1,2 @@ +from __future__ import absolute_import +from plotly.graph_objs import * diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_append_trace.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_append_trace.py index 40e5ac759ac..d90b8893408 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_append_trace.py +++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_append_trace.py @@ -111,22 +111,6 @@ def test_append_scatter(): assert d1 == d2 -@raises(Exception) -def test_append_scatter_after_deleting_xaxis(): - trace = Scatter(x=[1, 2, 3], y=[2, 3, 4]) - fig = tls.make_subplots(rows=2, cols=3) - fig['layout'].pop('xaxis5', None) - fig.append_trace(trace, 2, 2) - - -@raises(Exception) -def test_append_scatter_after_deleting_yaxis(): - trace = Scatter(x=[1, 2, 3], y=[2, 3, 4]) - fig = tls.make_subplots(rows=2, cols=3) - fig['layout'].pop('yaxis5', None) - fig.append_trace(trace, 2, 2) - - def test_append_scatter3d(): expected = Figure( data=Data([ @@ -168,13 +152,3 @@ def test_append_scatter3d(): d1, d2 = strip_dict_params(fig['layout'], expected['layout']) assert d1 == d2 - - -@raises(Exception) -def test_append_scatter3d_after_deleting_scene(): - fig = tls.make_subplots(rows=2, cols=1, - specs=[[{'is_3d': True}], - [{'is_3d': True}]]) - trace = Scatter3d(x=[1, 2, 3], y=[2, 3, 4], z=[1, 2, 3]) - fig['layout'].pop('scene1', None) - fig.append_trace(trace, 1, 1) diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py index c38cef4a9fc..527ee0dc85c 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py +++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -import plotly.graph_objs as go +import plotly.graph_objects as go from plotly.tests.utils import TestCaseNoTemplate @@ -139,3 +139,25 @@ def test_add_trace_underscore_kwarg(self): fig.add_scatter(y=[2, 1, 3], marker_line_color='green') self.assertEqual(fig.data[0].marker.line.color, 'green') + + def test_scalar_trace_as_data(self): + fig = go.Figure(data=go.Waterfall(y=[2, 1, 3])) + self.assertEqual(fig.data, (go.Waterfall(y=[2, 1, 3]),)) + + fig = go.Figure(data=dict(type='waterfall', y=[2, 1, 3])) + self.assertEqual(fig.data, (go.Waterfall(y=[2, 1, 3]),)) + + def test_pop_data(self): + fig = go.Figure(data=go.Waterfall(y=[2, 1, 3])) + self.assertEqual(fig.pop('data'), (go.Waterfall(y=[2, 1, 3]),)) + self.assertEqual(fig.data, ()) + + def test_pop_layout(self): + fig = go.Figure(layout=go.Layout(width=1000)) + self.assertEqual(fig.pop('layout'), go.Layout(width=1000)) + self.assertEqual(fig.layout, go.Layout()) + + def test_pop_invalid_key(self): + fig = go.Figure(layout=go.Layout(width=1000)) + with self.assertRaises(KeyError): + fig.pop('bogus') diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_graph_objs.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_graph_objs.py index 004ef74e8ee..2cf2f4ded78 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_graph_objs.py +++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_graph_objs.py @@ -115,3 +115,40 @@ def test_legacy_title_props_remapped(self): self.assertIn('titlefont', obj) self.assertIn('titlefont.family', obj) self.assertIn('titlefont', iter(obj)) + + +class TestPop(TestCase): + def setUp(self): + self.layout = go.Layout( + width=1000, + title={'text': 'the title', 'font': {'size': 20}}, + annotations=[{}, {}], + xaxis2={'range': [1, 2]} + ) + + def test_pop_valid_simple_prop(self): + self.assertEqual(self.layout.width, 1000) + self.assertEqual(self.layout.pop('width'), 1000) + self.assertIsNone(self.layout.width) + + def test_pop_valid_compound_prop(self): + val = self.layout.title + self.assertEqual(self.layout.pop('title'), val) + self.assertEqual(self.layout.title, go.layout.Title()) + + def test_pop_valid_array_prop(self): + val = self.layout.annotations + self.assertEqual(self.layout.pop('annotations'), val) + self.assertEqual(self.layout.annotations, ()) + + def test_pop_valid_subplot_prop(self): + val = self.layout.xaxis2 + self.assertEqual(self.layout.pop('xaxis2'), val) + self.assertEqual(self.layout.xaxis2, go.layout.XAxis()) + + def test_pop_invalid_prop_key_error(self): + with self.assertRaises(KeyError): + self.layout.pop('bogus') + + def test_pop_invalid_prop_with_default(self): + self.assertEqual(self.layout.pop('bogus', 42), 42) \ No newline at end of file diff --git a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py new file mode 100644 index 00000000000..1b152191c04 --- /dev/null +++ b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py @@ -0,0 +1,29 @@ +from unittest import TestCase +import plotly.graph_objects as go + + +class TestUpdateLayout(TestCase): + + def test_update_layout_kwargs(self): + # Create initial figure + fig = go.Figure() + fig.layout.title.font.size = 10 + + # Grab copy of original figure + orig_fig = go.Figure(fig) + + fig.update_layout(title_font_family='Courier New') + orig_fig.layout.update(title_font_family='Courier New') + self.assertEqual(fig, orig_fig) + + def test_update_layout_dict(self): + # Create initial figure + fig = go.Figure() + fig.layout.title.font.size = 10 + + # Grab copy of original figure + orig_fig = go.Figure(fig) + + fig.update_layout(dict(title=dict(font=dict(family='Courier New')))) + orig_fig.layout.update(title_font_family='Courier New') + self.assertEqual(fig, orig_fig) diff --git a/packages/python/plotly/plotly/tests/test_orca/test_to_image.py b/packages/python/plotly/plotly/tests/test_orca/test_to_image.py index d57d84bdfec..156ccc58237 100644 --- a/packages/python/plotly/plotly/tests/test_orca/test_to_image.py +++ b/packages/python/plotly/plotly/tests/test_orca/test_to_image.py @@ -321,7 +321,7 @@ def test_invalid_figure_json(): with pytest.raises(ValueError) as err: pio.to_image(bad_fig, format='png') - assert "Invalid value of type" in str(err.value) + assert "Invalid" in str(err.value) with pytest.raises(ValueError) as err: pio.to_image(bad_fig, format='png', validate=False) diff --git a/packages/python/plotly/plotlywidget/__init__.py b/packages/python/plotly/plotlywidget/__init__.py new file mode 100644 index 00000000000..421e7990307 --- /dev/null +++ b/packages/python/plotly/plotlywidget/__init__.py @@ -0,0 +1,9 @@ +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'static', + 'dest': 'plotlywidget', + 'require': 'plotlywidget/extension' + }] + +__frontend_version__ = '^0.1'