Repository: bokeh/bokeh

Issue:
Warn on setting empty plot.legend attrs
Is p.legend.location supposed to be dependent on object creation?  

```
import numpy as np
from bokeh.plotting import figure, show, output_file

x = np.linspace(0, 4*np.pi, 100)
y = np.sin(x)

output_file("legend_labels.html")

p = figure()

#figure attributes
p.title.text = 'Example1'

#set objects
p.circle(x, y, legend="sin(x)")
p.line(x, y, legend="sin(x)")

p.line(x, 2*y, legend="2*sin(x)",
       line_dash=[4, 4], line_color="orange", line_width=2)

p.square(x, 3*y, legend="3*sin(x)", fill_color=None, line_color="green")
p.line(x, 3*y, legend="3*sin(x)", line_color="green")

#dependent figure attributes?
p.legend.location = "bottom_left"

show(p)
```
result: Legend in correct location
![capture](https://cloud.githubusercontent.com/assets/17650526/21549901/324a1e26-cdbb-11e6-8446-9f139a8b4bd3.PNG)

```
import numpy as np
from bokeh.plotting import figure, show, output_file

x = np.linspace(0, 4*np.pi, 100)
y = np.sin(x)

output_file("legend_labels.html")

p = figure()

#figure attributes
p.title.text = 'Example2'
p.legend.location = "bottom_left" #no worky here

#set objects
p.circle(x, y, legend="sin(x)")
p.line(x, y, legend="sin(x)")

p.line(x, 2*y, legend="2*sin(x)",
       line_dash=[4, 4], line_color="orange", line_width=2)

p.square(x, 3*y, legend="3*sin(x)", fill_color=None, line_color="green")
p.line(x, 3*y, legend="3*sin(x)", line_color="green")

show(p)
```
result: Legend in default location
![capture](https://cloud.githubusercontent.com/assets/17650526/21549934/6de8b71c-cdbb-11e6-84a2-c50a1047356e.PNG)





Discussion:
The reason the other example doesn't work as expect is because on line
```
p.legend.location = "bottom_left" #no worky here
```
`location` is set not on `Legend`, but an instance of `_list_attr_splat`, that allows forwarding attributes over collections of objects. At this point no legend is actually available on `p`, so `_list_attr_splat` silently forwards `location` over an empty set. A legend is created as soon as `p.circle(x, y, legend="sin(x)")` is evaluated (or any plotting API's function with `legend` argument).

Given this implementation, what you observe is an expected, but not very helpful behavior.
Right, we could warn or error if setting on an empty list. 
Does it ever make sense to forward attributes to an empty set? Maybe besides some internal convenience code.  I would prefer to make it an error and create a legend early.
I just got hit by trying to setup my `Figure` before any glyphs, and the legend position not working:
```python
p = figure(...)
p.xaxis.axis_label = 'x-axis'
...
p.legend.location = 'top_left'  # Move this line...
p.patch(...)  # ...to after here!
output_file('broken.html')
save(p)
```
so I for one would vote for a warning!
> Does it ever make sense to forward attributes to an empty set? Maybe besides some internal convenience code. I would prefer to make it an error and create a legend early.

This would have to happen in the splattable list `__setattr__` but that means that it is necessary to configure the splattable list with some state so that it can call e.g. `add_layout(Legend())` on the right plot later. But it is not possible to add any state&mdash;the built-in `list` will not accept any new attributes, or even on subclasses. The best we can do is issue a warning, which is what I will do. 

Resolving patch:
diff --git a/bokeh/models/plots.py b/bokeh/models/plots.py
--- a/bokeh/models/plots.py
+++ b/bokeh/models/plots.py
@@ -3,6 +3,8 @@
 '''
 from __future__ import absolute_import
 
+import warnings
+
 from six import string_types
 
 from ..core.enums import Location, OutputBackend
@@ -41,6 +43,17 @@ def __dir__(self):
         else:
             return dir(self)
 
+_LEGEND_EMPTY_WARNING = """
+You are attemptings to set `plot.legend.%s` on a plot that has zero legends added, this will have no effect.
+
+Before legend properties can be set, you must add a Legend explicitly, or call a glyph method with the 'legend' parameter set.
+"""
+
+class _legend_attr_splat(_list_attr_splat):
+    def __setattr__(self, attr, value):
+        if not len(self):
+            warnings.warn(_LEGEND_EMPTY_WARNING % attr)
+        return super(_legend_attr_splat, self).__setattr__(attr, value)
 
 def _select_helper(args, kwargs):
     """ Allow flexible selector syntax.
@@ -202,7 +215,7 @@ def legend(self):
 
         '''
         legends = [obj for obj in self.renderers if isinstance(obj, Legend)]
-        return _list_attr_splat(legends)
+        return _legend_attr_splat(legends)
 
     @property
     def hover(self):


Test patch:
diff --git a/bokeh/models/tests/test_plots.py b/bokeh/models/tests/test_plots.py
--- a/bokeh/models/tests/test_plots.py
+++ b/bokeh/models/tests/test_plots.py
@@ -19,6 +19,32 @@
 from bokeh.models.scales import CategoricalScale, LinearScale, LogScale
 from bokeh.models.tools import PanTool
 
+import bokeh.models.plots as bmp
+
+_LEGEND_EMPTY_WARNING = """
+You are attemptings to set `plot.legend.location` on a plot that has zero legends added, this will have no effect.
+
+Before legend properties can be set, you must add a Legend explicitly, or call a glyph method with the 'legend' parameter set.
+"""
+
+class TestPlotLegendProperty(object):
+
+    def test_basic(self):
+        plot = figure(tools='')
+        x = plot.legend
+        assert isinstance(x, bmp._list_attr_splat)
+        assert len(x) == 0
+        plot.circle([1,2], [3,4], legend="foo")
+        x = plot.legend
+        assert isinstance(x, bmp._list_attr_splat)
+        assert len(x) == 1
+
+    def test_warnign(self):
+        plot = figure(tools='')
+        with pytest.warns(UserWarning) as warns:
+            plot.legend.location = "above"
+            assert len(warns) == 1
+            assert warns[0].message.args[0] == _LEGEND_EMPTY_WARNING
 
 class TestPlotSelect(object):