1#----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6#----------------------------------------------------------------------------- 7 8#----------------------------------------------------------------------------- 9# Boilerplate 10#----------------------------------------------------------------------------- 11import pytest ; pytest 12 13#----------------------------------------------------------------------------- 14# Imports 15#----------------------------------------------------------------------------- 16 17# External imports 18import mock 19from mock import patch 20 21# Bokeh imports 22from bokeh.core.validation import check_integrity 23from bokeh.models import ( 24 CategoricalScale, 25 CustomJS, 26 DataRange1d, 27 FactorRange, 28 GlyphRenderer, 29 Label, 30 LinearAxis, 31 LinearScale, 32 LogScale, 33 PanTool, 34 Plot, 35 Range1d, 36) 37from bokeh.plotting import figure 38 39# Module under test 40import bokeh.models.plots as bmp # isort:skip 41 42#----------------------------------------------------------------------------- 43# Setup 44#----------------------------------------------------------------------------- 45 46_LEGEND_EMPTY_WARNING = """ 47You are attempting to set `plot.legend.location` on a plot that has zero legends added, this will have no effect. 48 49Before legend properties can be set, you must add a Legend explicitly, or call a glyph method with a legend parameter set. 50""" 51 52#----------------------------------------------------------------------------- 53# General API 54#----------------------------------------------------------------------------- 55 56 57class TestPlotLegendProperty: 58 def test_basic(self) -> None: 59 plot = figure(tools='') 60 x = plot.legend 61 assert isinstance(x, bmp._list_attr_splat) 62 assert len(x) == 0 63 plot.circle([1,2], [3,4], legend_label="foo") 64 x = plot.legend 65 assert isinstance(x, bmp._list_attr_splat) 66 assert len(x) == 1 67 68 def test_warnign(self) -> None: 69 plot = figure(tools='') 70 with pytest.warns(UserWarning) as warns: 71 plot.legend.location = "above" 72 assert len(warns) == 1 73 assert warns[0].message.args[0] == _LEGEND_EMPTY_WARNING 74 75 76class TestPlotSelect: 77 def setup_method(self): 78 self._plot = figure(tools='pan') 79 self._plot.circle([1,2,3], [3,2,1], name='foo') 80 81 @patch('bokeh.models.plots.find') 82 def test_string_arg(self, mock_find) -> None: 83 self._plot.select('foo') 84 assert mock_find.called 85 assert mock_find.call_args[0][1] == dict(name='foo') 86 87 @patch('bokeh.models.plots.find') 88 def test_type_arg(self, mock_find) -> None: 89 self._plot.select(PanTool) 90 assert mock_find.called 91 assert mock_find.call_args[0][1] == dict(type=PanTool) 92 93 @patch('bokeh.models.plots.find') 94 def test_kwargs(self, mock_find) -> None: 95 kw = dict(name='foo', type=GlyphRenderer) 96 self._plot.select(**kw) 97 assert mock_find.called 98 assert mock_find.call_args[0][1] == kw 99 100 @patch('bokeh.models.plots.find') 101 def test_single_selector_kwarg(self, mock_find) -> None: 102 kw = dict(name='foo', type=GlyphRenderer) 103 self._plot.select(selector=kw) 104 assert mock_find.called 105 assert mock_find.call_args[0][1] == kw 106 107 def test_selector_kwarg_and_extra_kwargs(self) -> None: 108 with pytest.raises(TypeError) as exc: 109 self._plot.select(selector=dict(foo='foo'), bar='bar') 110 assert "when passing 'selector' keyword arg, not other keyword args may be present" == str(exc.value) 111 112 def test_bad_arg_type(self) -> None: 113 with pytest.raises(TypeError) as exc: 114 self._plot.select(10) 115 assert "selector must be a dictionary, string or plot object." == str(exc.value) 116 117 def test_too_many_args(self) -> None: 118 with pytest.raises(TypeError) as exc: 119 self._plot.select('foo', 'bar') 120 assert 'select accepts at most ONE positional argument.' == str(exc.value) 121 122 def test_no_input(self) -> None: 123 with pytest.raises(TypeError) as exc: 124 self._plot.select() 125 assert 'select requires EITHER a positional argument, OR keyword arguments.' == str(exc.value) 126 127 def test_arg_and_kwarg(self) -> None: 128 with pytest.raises(TypeError) as exc: 129 self._plot.select('foo', type=PanTool) 130 assert 'select accepts EITHER a positional argument, OR keyword arguments (not both).' == str(exc.value) 131 132 133class TestPlotValidation: 134 def test_missing_renderers(self) -> None: 135 p = figure() 136 p.renderers = [] 137 with mock.patch('bokeh.core.validation.check.log') as mock_logger: 138 check_integrity([p]) 139 assert mock_logger.warning.call_count == 1 140 assert mock_logger.warning.call_args[0][0].startswith("W-1000 (MISSING_RENDERERS): Plot has no renderers") 141 142 def test_missing_scale(self) -> None: 143 p = figure() 144 145 with pytest.raises(ValueError): 146 p.x_scale = None 147 148 with pytest.raises(ValueError): 149 p.y_scale = None 150 151 def test_missing_range(self) -> None: 152 p = figure() 153 154 with pytest.raises(ValueError): 155 p.x_range = None 156 157 with pytest.raises(ValueError): 158 p.y_range = None 159 160 def test_bad_extra_range_name(self) -> None: 161 p = figure() 162 p.xaxis.x_range_name="junk" 163 with mock.patch('bokeh.core.validation.check.log') as mock_logger: 164 check_integrity([p]) 165 assert mock_logger.error.call_count == 1 166 assert mock_logger.error.call_args[0][0].startswith( 167 "E-1020 (BAD_EXTRA_RANGE_NAME): An extra range name is configued with a name that does not correspond to any range: x_range_name='junk' [LinearAxis" 168 ) 169 170 p = figure() 171 p.extra_x_ranges['foo'] = Range1d() 172 p.grid.x_range_name="junk" 173 with mock.patch('bokeh.core.validation.check.log') as mock_logger: 174 check_integrity([p]) 175 assert mock_logger.error.call_count == 1 176 assert mock_logger.error.call_args[0][0].startswith( 177 "E-1020 (BAD_EXTRA_RANGE_NAME): An extra range name is configued with a name that does not correspond to any range: x_range_name='junk' [Grid" 178 ) 179 assert mock_logger.error.call_args[0][0].count("Grid") == 2 180 181 def test_bad_extra_range_only_immediate_refs(self) -> None: 182 # test whether adding a figure (*and* it's extra ranges) 183 # to another's references doesn't create a false positive 184 p, dep = figure(), figure() 185 dep.extra_x_ranges['foo'] = Range1d() 186 dep.grid.x_range_name="foo" 187 p.grid[0].js_on_change("dimension", CustomJS(code = "", args = {"toto": dep.grid[0]})) 188 with mock.patch('bokeh.core.validation.check.log') as mock_logger: 189 check_integrity([p]) 190 assert mock_logger.error.call_count == 0 191 192def test_plot_add_layout_raises_error_if_not_render() -> None: 193 plot = figure() 194 with pytest.raises(ValueError): 195 plot.add_layout(Range1d()) 196 197 198def test_plot_add_layout_adds_label_to_plot_renderers() -> None: 199 plot = figure() 200 label = Label() 201 plot.add_layout(label) 202 assert label in plot.center 203 204 205def test_plot_add_layout_adds_axis_to_renderers_and_side_renderers() -> None: 206 plot = figure() 207 axis = LinearAxis() 208 plot.add_layout(axis, 'left') 209 assert axis in plot.left 210 211 212def test_sizing_mode_property_is_fixed_by_default() -> None: 213 plot = figure() 214 assert plot.sizing_mode is None 215 216 217class BaseTwinAxis: 218 """Base class for testing extra ranges""" 219 220 def verify_axis(self, axis_name): 221 plot = Plot() 222 range_obj = getattr(plot, f"extra_{axis_name}_ranges") 223 range_obj["foo_range"] = self.get_range_instance() 224 assert range_obj["foo_range"] 225 226 def test_x_range(self) -> None: 227 self.verify_axis('x') 228 229 def test_y_range(self) -> None: 230 self.verify_axis('y') 231 232 @staticmethod 233 def get_range_instance(): 234 raise NotImplementedError 235 236 237class TestCategoricalTwinAxis(BaseTwinAxis): 238 """Test whether extra x and y ranges can be categorical""" 239 240 @staticmethod 241 def get_range_instance(): 242 return FactorRange('foo', 'bar') 243 244 245class TestLinearTwinAxis(BaseTwinAxis): 246 """Test whether extra x and y ranges can be Range1d""" 247 248 @staticmethod 249 def get_range_instance(): 250 return Range1d(0, 42) 251 252 253def test_plot_with_no_title_specified_creates_an_empty_title() -> None: 254 plot = Plot() 255 assert plot.title.text == "" 256 257 258def test_plot__scale_classmethod() -> None: 259 assert isinstance(Plot._scale("auto"), LinearScale) 260 assert isinstance(Plot._scale("linear"), LinearScale) 261 assert isinstance(Plot._scale("log"), LogScale) 262 assert isinstance(Plot._scale("categorical"), CategoricalScale) 263 with pytest.raises(ValueError): 264 Plot._scale("malformed_type") 265 266 267def test__check_required_scale_has_scales() -> None: 268 plot = Plot() 269 check = plot._check_required_scale() 270 assert check == [] 271 272 273def test__check_required_scale_missing_scales() -> None: 274 with pytest.raises(ValueError): 275 Plot(x_scale=None, y_scale=None) 276 277 278def test__check_compatible_scale_and_ranges_compat_numeric() -> None: 279 plot = Plot(x_scale=LinearScale(), x_range=Range1d()) 280 check = plot._check_compatible_scale_and_ranges() 281 assert check == [] 282 283 plot = Plot(y_scale=LogScale(), y_range=DataRange1d()) 284 check = plot._check_compatible_scale_and_ranges() 285 assert check == [] 286 287 288def test__check_compatible_scale_and_ranges_compat_factor() -> None: 289 plot = Plot(x_scale=CategoricalScale(), x_range=FactorRange()) 290 check = plot._check_compatible_scale_and_ranges() 291 assert check == [] 292 293 294def test__check_compatible_scale_and_ranges_incompat_numeric_scale_and_factor_range() -> None: 295 plot = Plot(x_scale=LinearScale(), x_range=FactorRange()) 296 check = plot._check_compatible_scale_and_ranges() 297 assert check != [] 298 299 300def test__check_compatible_scale_and_ranges_incompat_factor_scale_and_numeric_range() -> None: 301 plot = Plot(x_scale=CategoricalScale(), x_range=DataRange1d()) 302 check = plot._check_compatible_scale_and_ranges() 303 assert check != [] 304 305#----------------------------------------------------------------------------- 306# Dev API 307#----------------------------------------------------------------------------- 308 309#----------------------------------------------------------------------------- 310# Private API 311#----------------------------------------------------------------------------- 312 313 314class Test_list_attr_splat: 315 def test_set(self) -> None: 316 obj = bmp._list_attr_splat([DataRange1d(), DataRange1d()]) 317 assert len(obj) == 2 318 assert obj[0].start == None 319 assert obj[1].start == None 320 obj.start = 10 321 assert obj[0].start == 10 322 assert obj[1].start == 10 323 324 def test_set_empty(self) -> None: 325 obj = bmp._list_attr_splat([]) 326 assert len(obj) == 0 327 obj.start = 10 328 assert len(obj) == 0 329 330 def test_get_set_single(self) -> None: 331 p = figure() 332 assert len(p.xaxis) == 1 333 334 # check both ways to access 335 assert p.xaxis.formatter.power_limit_low != 100 336 assert p.xaxis[0].formatter.power_limit_low != 100 337 338 p.axis.formatter.power_limit_low = 100 339 340 assert p.xaxis.formatter.power_limit_low == 100 341 assert p.xaxis[0].formatter.power_limit_low == 100 342 343 def test_get_set_multi(self) -> None: 344 p = figure() 345 assert len(p.axis) == 2 346 347 # check both ways to access 348 assert p.axis[0].formatter.power_limit_low != 100 349 assert p.axis[1].formatter.power_limit_low != 100 350 assert p.axis.formatter[0].power_limit_low != 100 351 assert p.axis.formatter[1].power_limit_low != 100 352 353 p.axis.formatter.power_limit_low = 100 354 355 assert p.axis[0].formatter.power_limit_low == 100 356 assert p.axis[1].formatter.power_limit_low == 100 357 assert p.axis.formatter[0].power_limit_low == 100 358 assert p.axis.formatter[1].power_limit_low == 100 359 360 def test_get_set_multi_mismatch(self) -> None: 361 obj = bmp._list_attr_splat([LinearAxis(), FactorRange()]) 362 with pytest.raises(AttributeError) as e: 363 obj.formatter.power_limit_low == 10 364 assert str(e.value).endswith("list items have no %r attribute" % "formatter") 365 366 def test_get_empty(self) -> None: 367 obj = bmp._list_attr_splat([]) 368 with pytest.raises(AttributeError) as e: 369 obj.start 370 assert str(e.value).endswith("Trying to access %r attribute on an empty 'splattable' list" % "start") 371 372 def test_get_index(self) -> None: 373 obj = bmp._list_attr_splat([1, 2, 3]) 374 assert obj.index(2) == 1 375 376 def test_pop_value(self) -> None: 377 obj = bmp._list_attr_splat([1, 2, 3]) 378 obj.pop(1) 379 assert obj == [1, 3] 380 381#----------------------------------------------------------------------------- 382# Code 383#----------------------------------------------------------------------------- 384