Repository: plotly/plotly.py

Issue:
ColorScaleValidator should probably return a string when it's specified as a string.
Currently, color scale attributes have a kinda weird behavior:

```python
import plotly.graph_objs as go
go.scatter.Marker(colorscale="Viridis").colorscale
```
```
(('V',), ('i',), ('r',), ('i',), ('d',), ('i',), ('s',))
```

This is unlike the behavior (to the best of my knowledge) for other attributes specified as strings:

```python
go.scatter.Marker(color="red").color
```
```
'red'
```

And this is pretty unintuitive behavior when doing stuff like:

```python
marker1 = go.scatter.Marker(colorscale="Viridis")
go.scatter.Marker(colorscale=marker1.colorscale)
```
```
ValueError: 
    Invalid value of type 'builtins.tuple' received for the 'colorscale' property of scatter.marker
        Received value: (('V',), ('i',), ('r',), ('i',), ('d',), ('i',), ('s',))
```

Though `.update(marker1)` does work for some reason.

My guess is all that needs to happen is to add an `elif isinstance(v, str): return v` to this function, since you won't be breaking that immutability requirement.

https://github.com/plotly/plotly.py/blob/d670bb0f535fbd0a884c699aaa835ba624721859/_plotly_utils/basevalidators.py#L1276-L1281


Discussion:
Thanks for the report @ivirshup . Yeah, this is definitely an oversight. You're proposed fix should take care of it.  Feel free to submit a PR, otherwise we'll get to in the next few weeks. Thanks!

Resolving patch:
diff --git a/_plotly_utils/basevalidators.py b/_plotly_utils/basevalidators.py
--- a/_plotly_utils/basevalidators.py
+++ b/_plotly_utils/basevalidators.py
@@ -1211,7 +1211,7 @@ class ColorscaleValidator(BaseValidator):
     named_colorscales = [
         'Greys', 'YlGnBu', 'Greens', 'YlOrRd', 'Bluered', 'RdBu', 'Reds',
         'Blues', 'Picnic', 'Rainbow', 'Portland', 'Jet', 'Hot', 'Blackbody',
-        'Earth', 'Electric', 'Viridis'
+        'Earth', 'Electric', 'Viridis', 'Cividis'
     ]
 
     def __init__(self, plotly_name, parent_name, **kwargs):
@@ -1229,7 +1229,7 @@ def description(self):
       - One of the following named colorscales:
             ['Greys', 'YlGnBu', 'Greens', 'YlOrRd', 'Bluered', 'RdBu',
             'Reds', 'Blues', 'Picnic', 'Rainbow', 'Portland', 'Jet',
-            'Hot', 'Blackbody', 'Earth', 'Electric', 'Viridis']
+            'Hot', 'Blackbody', 'Earth', 'Electric', 'Viridis', 'Cividis]
         """.format(plotly_name=self.plotly_name)
 
         return desc
@@ -1274,9 +1274,11 @@ def validate_coerce(self, v):
         return v
 
     def present(self, v):
-        # Return tuple of tuples so that colorscale is immutable
+        # Return-type must be immutable
         if v is None:
             return None
+        elif isinstance(v, string_types):
+            return v
         else:
             return tuple([tuple(e) for e in v])
 


Test patch:
diff --git a/_plotly_utils/tests/validators/test_colorscale_validator.py b/_plotly_utils/tests/validators/test_colorscale_validator.py
--- a/_plotly_utils/tests/validators/test_colorscale_validator.py
+++ b/_plotly_utils/tests/validators/test_colorscale_validator.py
@@ -11,7 +11,8 @@ def validator():
 
 
 @pytest.fixture(params=['Greys', 'YlGnBu', 'Greens', 'YlOrRd', 'Bluered', 'RdBu', 'Reds', 'Blues',
-                        'Picnic', 'Rainbow', 'Portland', 'Jet', 'Hot', 'Blackbody', 'Earth', 'Electric', 'Viridis'])
+                        'Picnic', 'Rainbow', 'Portland', 'Jet', 'Hot', 'Blackbody', 'Earth', 'Electric', 
+                        'Viridis', 'Cividis'])
 def named_colorscale(request):
     return request.param
 
@@ -26,6 +27,8 @@ def test_acceptance_named(named_colorscale, validator: ColorscaleValidator):
     # Uppercase
     assert (validator.validate_coerce(named_colorscale.upper()) ==
            named_colorscale.upper())
+    
+    assert validator.present(named_colorscale) == named_colorscale
 
 # ### Acceptance as array ###
 @pytest.mark.parametrize('val', [
diff --git a/plotly/tests/test_core/test_graph_objs/test_properties_validated.py b/plotly/tests/test_core/test_graph_objs/test_properties_validated.py
--- a/plotly/tests/test_core/test_graph_objs/test_properties_validated.py
+++ b/plotly/tests/test_core/test_graph_objs/test_properties_validated.py
@@ -102,6 +102,16 @@ def test_present_colorscale(self):
         # Presented as tuple of tuples
         self.assertEqual(self.scatter.marker.colorscale,
                          ((0, 'red'), (1, 'green')))
+    
+    def test_present_colorscale_str(self):
+        self.assertIsNone(self.scatter.marker.colorscale)
+
+        # Assign string
+        self.scatter.marker.colorscale = "Viridis"
+
+        # Presented as a string
+        self.assertEqual(self.scatter.marker.colorscale,
+                         "Viridis")
 
 
 class TestPropertyIterContains(TestCase):