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