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# Standard library imports 18import logging 19 20# External imports 21from mock import patch 22 23# Bokeh imports 24from bokeh import __version__ 25from bokeh.core.properties import Instance, Int, List, String 26from bokeh.document.document import Document 27from bokeh.events import Tap 28from bokeh.io import curdoc 29from bokeh.model import Model 30from bokeh.themes import Theme 31from bokeh.util.logconfig import basicConfig 32 33# Module under test 34import bokeh.embed.util as beu # isort:skip 35 36#----------------------------------------------------------------------------- 37# Setup 38#----------------------------------------------------------------------------- 39 40@pytest.fixture 41def test_plot() -> None: 42 from bokeh.plotting import figure 43 test_plot = figure() 44 test_plot.circle([1, 2], [2, 3]) 45 return test_plot 46 47class SomeModel(Model): 48 some = Int 49 50class OtherModel(Model): 51 child = Instance(Model) 52 53# Taken from test_callback_manager.py 54class _GoodPropertyCallback: 55 def __init__(self): 56 self.last_name = None 57 self.last_old = None 58 self.last_new = None 59 60 def __call__(self, name, old, new): 61 self.method(name, old, new) 62 63 def method(self, name, old, new): 64 self.last_name = name 65 self.last_old = old 66 self.last_new = new 67 68 def partially_good(self, name, old, new, newer): 69 pass 70 71 def just_fine(self, name, old, new, extra='default'): 72 pass 73 74 75class _GoodEventCallback: 76 def __init__(self): 77 self.last_name = None 78 self.last_old = None 79 self.last_new = None 80 81 def __call__(self, event): 82 self.method(event) 83 84 def method(self, event): 85 self.event = event 86 87 def partially_good(self, arg, event): 88 pass 89 90# Taken from test_model 91class EmbedTestUtilModel(Model): 92 a = Int(12) 93 b = String("hello") 94 c = List(Int, [1, 2, 3]) 95 96#----------------------------------------------------------------------------- 97# General API 98#----------------------------------------------------------------------------- 99 100#----------------------------------------------------------------------------- 101# Dev API 102#----------------------------------------------------------------------------- 103 104 105class Test_FromCurdoc: 106 def test_type(self) -> None: 107 assert isinstance(beu.FromCurdoc, type) 108 109_ODFERR = "OutputDocumentFor expects a sequence of Models" 110 111 112class Test_OutputDocumentFor_general: 113 def test_error_on_empty_list(self) -> None: 114 with pytest.raises(ValueError) as e: 115 with beu.OutputDocumentFor([]): 116 pass 117 assert str(e.value).endswith(_ODFERR) 118 119 def test_error_on_mixed_list(self) -> None: 120 p = SomeModel() 121 d = Document() 122 orig_theme = d.theme 123 with pytest.raises(ValueError) as e: 124 with beu.OutputDocumentFor([p, d]): 125 pass 126 assert str(e.value).endswith(_ODFERR) 127 assert d.theme is orig_theme 128 129 @pytest.mark.parametrize('v', [10, -0,3, "foo", True]) 130 def test_error_on_wrong_types(self, v) -> None: 131 with pytest.raises(ValueError) as e: 132 with beu.OutputDocumentFor(v): 133 pass 134 assert str(e.value).endswith(_ODFERR) 135 136 def test_with_doc_in_child_raises_error(self) -> None: 137 doc = Document() 138 p1 = SomeModel() 139 p2 = OtherModel(child=SomeModel()) 140 doc.add_root(p2.child) 141 assert p1.document is None 142 assert p2.document is None 143 assert p2.child.document is doc 144 with pytest.raises(RuntimeError) as e: 145 with beu.OutputDocumentFor([p1, p2]): 146 pass 147 assert "already in a doc" in str(e.value) 148 149 @patch('bokeh.document.document.check_integrity') 150 def test_validates_document_by_default(self, check_integrity, test_plot) -> None: 151 with beu.OutputDocumentFor([test_plot]): 152 pass 153 assert check_integrity.called 154 155 @patch('bokeh.document.document.check_integrity') 156 def test_doesnt_validate_doc_due_to_env_var(self, check_integrity, monkeypatch, test_plot) -> None: 157 monkeypatch.setenv("BOKEH_VALIDATE_DOC", "false") 158 with beu.OutputDocumentFor([test_plot]): 159 pass 160 assert not check_integrity.called 161 162 163class Test_OutputDocumentFor_default_apply_theme: 164 def test_single_model_with_document(self) -> None: 165 # should use existing doc in with-block 166 p = SomeModel() 167 d = Document() 168 orig_theme = d.theme 169 d.add_root(p) 170 with beu.OutputDocumentFor([p]): 171 assert p.document is d 172 assert d.theme is orig_theme 173 assert p.document is d 174 assert d.theme is orig_theme 175 176 def test_single_model_with_no_document(self) -> None: 177 p = SomeModel() 178 assert p.document is None 179 with beu.OutputDocumentFor([p]): 180 assert p.document is not None 181 assert p.document is not None 182 183 def test_list_of_model_with_no_documents(self) -> None: 184 # should create new (permanent) doc for inputs 185 p1 = SomeModel() 186 p2 = SomeModel() 187 assert p1.document is None 188 assert p2.document is None 189 with beu.OutputDocumentFor([p1, p2]): 190 assert p1.document is not None 191 assert p2.document is not None 192 assert p1.document is p2.document 193 new_doc = p1.document 194 new_theme = p1.document.theme 195 assert p1.document is new_doc 196 assert p1.document is p2.document 197 assert p1.document.theme is new_theme 198 199 def test_list_of_model_same_as_roots(self) -> None: 200 # should use existing doc in with-block 201 p1 = SomeModel() 202 p2 = SomeModel() 203 d = Document() 204 orig_theme = d.theme 205 d.add_root(p1) 206 d.add_root(p2) 207 with beu.OutputDocumentFor([p1, p2]): 208 assert p1.document is d 209 assert p2.document is d 210 assert d.theme is orig_theme 211 assert p1.document is d 212 assert p2.document is d 213 assert d.theme is orig_theme 214 215 def test_list_of_model_same_as_roots_with_always_new(self) -> None: 216 # should use new temp doc for everything inside with-block 217 p1 = SomeModel() 218 p2 = SomeModel() 219 d = Document() 220 orig_theme = d.theme 221 d.add_root(p1) 222 d.add_root(p2) 223 with beu.OutputDocumentFor([p1, p2], always_new=True): 224 assert p1.document is not d 225 assert p2.document is not d 226 assert p1.document is p2.document 227 assert p2.document.theme is orig_theme 228 assert p1.document is d 229 assert p2.document is d 230 assert d.theme is orig_theme 231 232 def test_list_of_model_subset_roots(self) -> None: 233 # should use new temp doc for subset inside with-block 234 p1 = SomeModel() 235 p2 = SomeModel() 236 d = Document() 237 orig_theme = d.theme 238 d.add_root(p1) 239 d.add_root(p2) 240 with beu.OutputDocumentFor([p1]): 241 assert p1.document is not d 242 assert p2.document is d 243 assert p1.document.theme is orig_theme 244 assert p2.document.theme is orig_theme 245 assert p1.document is d 246 assert p2.document is d 247 assert d.theme is orig_theme 248 249 def test_list_of_models_different_docs(self) -> None: 250 # should use new temp doc for eveything inside with-block 251 d = Document() 252 orig_theme = d.theme 253 p1 = SomeModel() 254 p2 = SomeModel() 255 d.add_root(p2) 256 assert p1.document is None 257 assert p2.document is not None 258 with beu.OutputDocumentFor([p1, p2]): 259 assert p1.document is not None 260 assert p2.document is not None 261 assert p1.document is not d 262 assert p2.document is not d 263 assert p1.document == p2.document 264 assert p1.document.theme is orig_theme 265 assert p1.document is None 266 assert p2.document is not None 267 assert p2.document.theme is orig_theme 268 269 270class Test_OutputDocumentFor_custom_apply_theme: 271 def test_single_model_with_document(self) -> None: 272 # should use existing doc in with-block 273 p = SomeModel() 274 d = Document() 275 orig_theme = d.theme 276 d.add_root(p) 277 with beu.OutputDocumentFor([p], apply_theme=Theme(json={})): 278 assert p.document is d 279 assert d.theme is not orig_theme 280 assert p.document is d 281 assert d.theme is orig_theme 282 283 def test_single_model_with_no_document(self) -> None: 284 p = SomeModel() 285 assert p.document is None 286 with beu.OutputDocumentFor([p], apply_theme=Theme(json={})): 287 assert p.document is not None 288 new_theme = p.document.theme 289 assert p.document is not None 290 assert p.document.theme is not new_theme 291 292 def test_list_of_model_with_no_documents(self) -> None: 293 # should create new (permanent) doc for inputs 294 p1 = SomeModel() 295 p2 = SomeModel() 296 assert p1.document is None 297 assert p2.document is None 298 with beu.OutputDocumentFor([p1, p2], apply_theme=Theme(json={})): 299 assert p1.document is not None 300 assert p2.document is not None 301 assert p1.document is p2.document 302 new_doc = p1.document 303 new_theme = p1.document.theme 304 assert p1.document is new_doc 305 assert p2.document is new_doc 306 assert p1.document is p2.document 307 # should restore to default theme after with-block 308 assert p1.document.theme is not new_theme 309 310 def test_list_of_model_same_as_roots(self) -> None: 311 # should use existing doc in with-block 312 p1 = SomeModel() 313 p2 = SomeModel() 314 d = Document() 315 orig_theme = d.theme 316 d.add_root(p1) 317 d.add_root(p2) 318 with beu.OutputDocumentFor([p1, p2], apply_theme=Theme(json={})): 319 assert p1.document is d 320 assert p2.document is d 321 assert d.theme is not orig_theme 322 assert p1.document is d 323 assert p2.document is d 324 assert d.theme is orig_theme 325 326 def test_list_of_model_same_as_roots_with_always_new(self) -> None: 327 # should use new temp doc for everything inside with-block 328 p1 = SomeModel() 329 p2 = SomeModel() 330 d = Document() 331 orig_theme = d.theme 332 d.add_root(p1) 333 d.add_root(p2) 334 with beu.OutputDocumentFor([p1, p2], always_new=True, apply_theme=Theme(json={})): 335 assert p1.document is not d 336 assert p2.document is not d 337 assert p1.document is p2.document 338 assert p2.document.theme is not orig_theme 339 assert p1.document is d 340 assert p2.document is d 341 assert d.theme is orig_theme 342 343 def test_list_of_model_subset_roots(self) -> None: 344 # should use new temp doc for subset inside with-block 345 p1 = SomeModel() 346 p2 = SomeModel() 347 d = Document() 348 orig_theme = d.theme 349 d.add_root(p1) 350 d.add_root(p2) 351 with beu.OutputDocumentFor([p1], apply_theme=Theme(json={})): 352 assert p1.document is not d 353 assert p2.document is d 354 assert p1.document.theme is not orig_theme 355 assert p2.document.theme is orig_theme 356 assert p1.document is d 357 assert p2.document is d 358 assert d.theme is orig_theme 359 360 def test_list_of_models_different_docs(self) -> None: 361 # should use new temp doc for eveything inside with-block 362 d = Document() 363 orig_theme = d.theme 364 p1 = SomeModel() 365 p2 = SomeModel() 366 d.add_root(p2) 367 assert p1.document is None 368 assert p2.document is not None 369 with beu.OutputDocumentFor([p1, p2], apply_theme=Theme(json={})): 370 assert p1.document is not None 371 assert p2.document is not None 372 assert p1.document is not d 373 assert p2.document is not d 374 assert p1.document == p2.document 375 assert p1.document.theme is not orig_theme 376 assert p1.document is None 377 assert p2.document is not None 378 assert p2.document.theme is orig_theme 379 380 381class Test_OutputDocumentFor_FromCurdoc_apply_theme: 382 def setup_method(self): 383 self.orig_theme = curdoc().theme 384 curdoc().theme = Theme(json={}) 385 386 def teardown_method(self): 387 curdoc().theme = self.orig_theme 388 389 def test_single_model_with_document(self) -> None: 390 # should use existing doc in with-block 391 p = SomeModel() 392 d = Document() 393 orig_theme = d.theme 394 d.add_root(p) 395 with beu.OutputDocumentFor([p], apply_theme=beu.FromCurdoc): 396 assert p.document is d 397 assert d.theme is curdoc().theme 398 assert p.document is d 399 assert d.theme is orig_theme 400 401 def test_single_model_with_no_document(self) -> None: 402 p = SomeModel() 403 assert p.document is None 404 with beu.OutputDocumentFor([p], apply_theme=beu.FromCurdoc): 405 assert p.document is not None 406 assert p.document.theme is curdoc().theme 407 new_doc = p.document 408 assert p.document is new_doc 409 assert p.document.theme is not curdoc().theme 410 411 def test_list_of_model_with_no_documents(self) -> None: 412 # should create new (permanent) doc for inputs 413 p1 = SomeModel() 414 p2 = SomeModel() 415 assert p1.document is None 416 assert p2.document is None 417 with beu.OutputDocumentFor([p1, p2], apply_theme=beu.FromCurdoc): 418 assert p1.document is not None 419 assert p2.document is not None 420 assert p1.document is p2.document 421 new_doc = p1.document 422 assert p1.document.theme is curdoc().theme 423 assert p1.document is new_doc 424 assert p2.document is new_doc 425 assert p1.document is p2.document 426 # should restore to default theme after with-block 427 assert p1.document.theme is not curdoc().theme 428 429 def test_list_of_model_same_as_roots(self) -> None: 430 # should use existing doc in with-block 431 p1 = SomeModel() 432 p2 = SomeModel() 433 d = Document() 434 orig_theme = d.theme 435 d.add_root(p1) 436 d.add_root(p2) 437 with beu.OutputDocumentFor([p1, p2], apply_theme=beu.FromCurdoc): 438 assert p1.document is d 439 assert p2.document is d 440 assert d.theme is curdoc().theme 441 assert p1.document is d 442 assert p2.document is d 443 assert d.theme is orig_theme 444 445 def test_list_of_model_same_as_roots_with_always_new(self) -> None: 446 # should use new temp doc for everything inside with-block 447 p1 = SomeModel() 448 p2 = SomeModel() 449 d = Document() 450 orig_theme = d.theme 451 d.add_root(p1) 452 d.add_root(p2) 453 with beu.OutputDocumentFor([p1, p2], always_new=True, apply_theme=beu.FromCurdoc): 454 assert p1.document is not d 455 assert p2.document is not d 456 assert p1.document is p2.document 457 assert p2.document.theme is curdoc().theme 458 assert p1.document is d 459 assert p2.document is d 460 assert d.theme is orig_theme 461 462 def test_list_of_model_subset_roots(self) -> None: 463 # should use new temp doc for subset inside with-block 464 p1 = SomeModel() 465 p2 = SomeModel() 466 d = Document() 467 orig_theme = d.theme 468 d.add_root(p1) 469 d.add_root(p2) 470 with beu.OutputDocumentFor([p1], apply_theme=beu.FromCurdoc): 471 assert p1.document is not d 472 assert p2.document is d 473 assert p1.document.theme is curdoc().theme 474 assert p2.document.theme is orig_theme 475 assert p1.document is d 476 assert p2.document is d 477 assert d.theme is orig_theme 478 479 def test_list_of_models_different_docs(self) -> None: 480 # should use new temp doc for eveything inside with-block 481 d = Document() 482 orig_theme = d.theme 483 p1 = SomeModel() 484 p2 = SomeModel() 485 d.add_root(p2) 486 assert p1.document is None 487 assert p2.document is not None 488 with beu.OutputDocumentFor([p1, p2], apply_theme=beu.FromCurdoc): 489 assert p1.document is not None 490 assert p2.document is not None 491 assert p1.document is not d 492 assert p2.document is not d 493 assert p1.document == p2.document 494 assert p1.document.theme is curdoc().theme 495 assert p1.document is None 496 assert p2.document is not None 497 assert p2.document.theme is orig_theme 498 499 500class Test_standalone_docs_json_and_render_items: 501 def test_passing_model(self) -> None: 502 p1 = SomeModel() 503 d = Document() 504 d.add_root(p1) 505 docs_json, render_items = beu.standalone_docs_json_and_render_items([p1]) 506 doc = list(docs_json.values())[0] 507 assert doc['title'] == "Bokeh Application" 508 assert doc['version'] == __version__ 509 assert len(doc['roots']['root_ids']) == 1 510 assert len(doc['roots']['references']) == 1 511 assert doc['roots']['references'] == [{'attributes': {}, 'id': str(p1.id), 'type': 'test_util__embed.SomeModel'}] 512 assert len(render_items) == 1 513 514 def test_passing_doc(self) -> None: 515 p1 = SomeModel() 516 d = Document() 517 d.add_root(p1) 518 docs_json, render_items = beu.standalone_docs_json_and_render_items([d]) 519 doc = list(docs_json.values())[0] 520 assert doc['title'] == "Bokeh Application" 521 assert doc['version'] == __version__ 522 assert len(doc['roots']['root_ids']) == 1 523 assert len(doc['roots']['references']) == 1 524 assert doc['roots']['references'] == [{'attributes': {}, 'id': str(p1.id), 'type': 'test_util__embed.SomeModel'}] 525 assert len(render_items) == 1 526 527 def test_exception_for_missing_doc(self) -> None: 528 p1 = SomeModel() 529 with pytest.raises(ValueError) as e: 530 beu.standalone_docs_json_and_render_items([p1]) 531 assert str(e.value) == "A Bokeh Model must be part of a Document to render as standalone content" 532 533 def test_log_warning_if_python_property_callback(self, caplog) -> None: 534 d = Document() 535 m1 = EmbedTestUtilModel() 536 c1 = _GoodPropertyCallback() 537 d.add_root(m1) 538 539 m1.on_change('name', c1) 540 assert len(m1._callbacks) != 0 541 542 with caplog.at_level(logging.WARN): 543 beu.standalone_docs_json_and_render_items(m1) 544 assert len(caplog.records) == 1 545 assert caplog.text != '' 546 547 def test_log_warning_if_python_event_callback(self, caplog) -> None: 548 d = Document() 549 m1 = EmbedTestUtilModel() 550 c1 = _GoodEventCallback() 551 d.add_root(m1) 552 553 m1.on_event(Tap, c1) 554 assert len(m1._event_callbacks) != 0 555 556 with caplog.at_level(logging.WARN): 557 beu.standalone_docs_json_and_render_items(m1) 558 assert len(caplog.records) == 1 559 assert caplog.text != '' 560 561 def test_suppress_warnings(self, caplog) -> None: 562 d = Document() 563 m1 = EmbedTestUtilModel() 564 c1 = _GoodPropertyCallback() 565 c2 = _GoodEventCallback() 566 d.add_root(m1) 567 568 m1.on_change('name', c1) 569 assert len(m1._callbacks) != 0 570 571 m1.on_event(Tap, c2) 572 assert len(m1._event_callbacks) != 0 573 574 with caplog.at_level(logging.WARN): 575 beu.standalone_docs_json_and_render_items(m1, suppress_callback_warning=True) 576 assert len(caplog.records) == 0 577 assert caplog.text == '' 578 579 580class Test_standalone_docs_json: 581 @patch('bokeh.embed.util.standalone_docs_json_and_render_items') 582 def test_delgation(self, mock_sdjari) -> None: 583 p1 = SomeModel() 584 p2 = SomeModel() 585 d = Document() 586 d.add_root(p1) 587 d.add_root(p2) 588 # ignore error unpacking None mock result, just checking to see that 589 # standalone_docs_json_and_render_items is called as expected 590 try: 591 beu.standalone_docs_json([p1, p2]) 592 except ValueError: 593 pass 594 mock_sdjari.assert_called_once_with([p1, p2]) 595 596 def test_output(self) -> None: 597 p1 = SomeModel() 598 p2 = SomeModel() 599 d = Document() 600 d.add_root(p1) 601 d.add_root(p2) 602 out = beu.standalone_docs_json([p1, p2]) 603 expected = beu.standalone_docs_json_and_render_items([p1, p2])[0] 604 assert list(out.values()) ==list(expected.values()) 605 606#----------------------------------------------------------------------------- 607# Private API 608#----------------------------------------------------------------------------- 609 610 611class Test__create_temp_doc: 612 def test_no_docs(self) -> None: 613 p1 = SomeModel() 614 p2 = SomeModel() 615 beu._create_temp_doc([p1, p2]) 616 assert isinstance(p1.document, Document) 617 assert isinstance(p2.document, Document) 618 619 def test_top_level_same_doc(self) -> None: 620 d = Document() 621 p1 = SomeModel() 622 p2 = SomeModel() 623 d.add_root(p1) 624 d.add_root(p2) 625 beu._create_temp_doc([p1, p2]) 626 assert isinstance(p1.document, Document) 627 assert p1.document is not d 628 assert isinstance(p2.document, Document) 629 assert p2.document is not d 630 631 assert p2.document == p1.document 632 633 def test_top_level_different_doc(self) -> None: 634 d1 = Document() 635 d2 = Document() 636 p1 = SomeModel() 637 p2 = SomeModel() 638 d1.add_root(p1) 639 d2.add_root(p2) 640 beu._create_temp_doc([p1, p2]) 641 assert isinstance(p1.document, Document) 642 assert p1.document is not d1 643 assert isinstance(p2.document, Document) 644 assert p2.document is not d2 645 646 assert p2.document == p1.document 647 648 def test_child_docs(self) -> None: 649 d = Document() 650 p1 = SomeModel() 651 p2 = OtherModel(child=SomeModel()) 652 d.add_root(p2.child) 653 beu._create_temp_doc([p1, p2]) 654 655 assert isinstance(p1.document, Document) 656 assert p1.document is not d 657 assert isinstance(p2.document, Document) 658 assert p2.document is not d 659 assert isinstance(p2.child.document, Document) 660 assert p2.child.document is not d 661 662 assert p2.document == p1.document 663 assert p2.document == p2.child.document 664 665 666class Test__dispose_temp_doc: 667 def test_no_docs(self) -> None: 668 p1 = SomeModel() 669 p2 = SomeModel() 670 beu._dispose_temp_doc([p1, p2]) 671 assert p1.document is None 672 assert p2.document is None 673 674 def test_with_docs(self) -> None: 675 d1 = Document() 676 d2 = Document() 677 p1 = SomeModel() 678 d1.add_root(p1) 679 p2 = OtherModel(child=SomeModel()) 680 d2.add_root(p2.child) 681 beu._create_temp_doc([p1, p2]) 682 beu._dispose_temp_doc([p1, p2]) 683 assert p1.document is d1 684 assert p2.document is None 685 assert p2.child.document is d2 686 687 def test_with_temp_docs(self) -> None: 688 p1 = SomeModel() 689 p2 = SomeModel() 690 beu._create_temp_doc([p1, p2]) 691 beu._dispose_temp_doc([p1, p2]) 692 assert p1.document is None 693 assert p2.document is None 694 695class Test__set_temp_theme: 696 def test_apply_None(self) -> None: 697 d = Document() 698 orig = d.theme 699 beu._set_temp_theme(d, None) 700 assert d._old_theme is orig 701 assert d.theme is orig 702 703 def test_apply_theme(self) -> None: 704 t = Theme(json={}) 705 d = Document() 706 orig = d.theme 707 beu._set_temp_theme(d, t) 708 assert d._old_theme is orig 709 assert d.theme is t 710 711 def test_apply_from_curdoc(self) -> None: 712 t = Theme(json={}) 713 curdoc().theme = t 714 d = Document() 715 orig = d.theme 716 beu._set_temp_theme(d, beu.FromCurdoc) 717 assert d._old_theme is orig 718 assert d.theme is t 719 720class Test__unset_temp_theme: 721 def test_basic(self) -> None: 722 t = Theme(json={}) 723 d = Document() 724 d._old_theme = t 725 beu._unset_temp_theme(d) 726 assert d.theme is t 727 assert not hasattr(d, "_old_theme") 728 729 def test_no_old_theme(self) -> None: 730 d = Document() 731 orig = d.theme 732 beu._unset_temp_theme(d) 733 assert d.theme is orig 734 assert not hasattr(d, "_old_theme") 735 736#----------------------------------------------------------------------------- 737# Code 738#----------------------------------------------------------------------------- 739 740# needed for caplog tests to function 741basicConfig() 742