1
"""
2
Defines the baseclasses that make a component render to a bokeh model
3
and become viewable including:
4

5
* Layoutable: Defines parameters concerned with layout and style
6
* ServableMixin: Mixin class that defines methods to serve object on server
7
* Renderable: Defines methods to render a component as a bokeh model
8
* Viewable: Defines methods to view the component in the
9
  notebook, on the server or in static exports
10
"""
11 2
import logging
12 2
import sys
13 2
import traceback
14 2
import uuid
15

16 2
from functools import partial
17

18 2
import param
19

20 2
from bokeh.document.document import Document as _Document
21 2
from bokeh.io.doc import curdoc as _curdoc
22 2
from pyviz_comms import JupyterCommManager
23

24 2
from .config import config, panel_extension
25 2
from .io.embed import embed_state
26 2
from .io.model import add_to_doc, patch_cds_msg
27 2
from .io.notebook import (
28
    ipywidget, render_mimebundle, render_model, show_embed, show_server
29
)
30 2
from .io.save import save
31 2
from .io.state import state
32 2
from .io.server import serve
33 2
from .util import escape, param_reprs
34

35

36 2
class Layoutable(param.Parameterized):
37
    """
38
    Layoutable defines shared style and layout related parameters
39
    for all Panel components with a visual representation.
40
    """
41

42 2
    align = param.ObjectSelector(default='start',
43
                                 objects=['start', 'end', 'center'], doc="""
44
        Whether the object should be aligned with the start, end or
45
        center of its container""")
46

47 2
    aspect_ratio = param.Parameter(default=None, doc="""
48
        Describes the proportional relationship between component's
49
        width and height.  This works if any of component's dimensions
50
        are flexible in size. If set to a number, ``width / height =
51
        aspect_ratio`` relationship will be maintained.  Otherwise, if
52
        set to ``"auto"``, component's preferred width and height will
53
        be used to determine the aspect (if not set, no aspect will be
54
        preserved).""")
55

56 2
    background = param.Parameter(default=None, doc="""
57
        Background color of the component.""")
58

59 2
    css_classes = param.List(default=None, doc="""
60
        CSS classes to apply to the layout.""")
61

62 2
    width = param.Integer(default=None, bounds=(0, None), doc="""
63
        The width of the component (in pixels). This can be either
64
        fixed or preferred width, depending on width sizing policy.""")
65

66 2
    height = param.Integer(default=None, bounds=(0, None), doc="""
67
        The height of the component (in pixels).  This can be either
68
        fixed or preferred height, depending on height sizing policy.""")
69

70 2
    min_width = param.Integer(default=None, bounds=(0, None), doc="""
71
        Minimal width of the component (in pixels) if width is adjustable.""")
72

73 2
    min_height = param.Integer(default=None, bounds=(0, None), doc="""
74
        Minimal height of the component (in pixels) if height is adjustable.""")
75

76 2
    max_width = param.Integer(default=None, bounds=(0, None), doc="""
77
        Minimal width of the component (in pixels) if width is adjustable.""")
78

79 2
    max_height = param.Integer(default=None, bounds=(0, None), doc="""
80
        Minimal height of the component (in pixels) if height is adjustable.""")
81

82 2
    margin = param.Parameter(default=5, doc="""
83
        Allows to create additional space around the component. May
84
        be specified as a two-tuple of the form (vertical, horizontal)
85
        or a four-tuple (top, right, bottom, left).""")
86

87 2
    width_policy = param.ObjectSelector(
88
        default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc="""
89
        Describes how the component should maintain its width.
90

91
        ``"auto"``
92
            Use component's preferred sizing policy.
93

94
        ``"fixed"``
95
            Use exactly ``width`` pixels. Component will overflow if
96
            it can't fit in the available horizontal space.
97

98
        ``"fit"``
99
            Use component's preferred width (if set) and allow it to
100
            fit into the available horizontal space within the minimum
101
            and maximum width bounds (if set). Component's width
102
            neither will be aggressively minimized nor maximized.
103

104
        ``"min"``
105
            Use as little horizontal space as possible, not less than
106
            the minimum width (if set).  The starting point is the
107
            preferred width (if set). The width of the component may
108
            shrink or grow depending on the parent layout, aspect
109
            management and other factors.
110

111
        ``"max"``
112
            Use as much horizontal space as possible, not more than
113
            the maximum width (if set).  The starting point is the
114
            preferred width (if set). The width of the component may
115
            shrink or grow depending on the parent layout, aspect
116
            management and other factors.
117
    """)
118

119 2
    height_policy = param.ObjectSelector(
120
        default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc="""
121
        Describes how the component should maintain its height.
122

123
        ``"auto"``
124
            Use component's preferred sizing policy.
125

126
        ``"fixed"``
127
            Use exactly ``height`` pixels. Component will overflow if
128
            it can't fit in the available vertical space.
129

130
        ``"fit"``
131
            Use component's preferred height (if set) and allow to fit
132
            into the available vertical space within the minimum and
133
            maximum height bounds (if set). Component's height neither
134
            will be aggressively minimized nor maximized.
135

136
        ``"min"``
137
            Use as little vertical space as possible, not less than
138
            the minimum height (if set).  The starting point is the
139
            preferred height (if set). The height of the component may
140
            shrink or grow depending on the parent layout, aspect
141
            management and other factors.
142

143
        ``"max"``
144
            Use as much vertical space as possible, not more than the
145
            maximum height (if set).  The starting point is the
146
            preferred height (if set). The height of the component may
147
            shrink or grow depending on the parent layout, aspect
148
            management and other factors.
149
    """)
150

151 2
    sizing_mode = param.ObjectSelector(default=None, objects=[
152
        'fixed', 'stretch_width', 'stretch_height', 'stretch_both',
153
        'scale_width', 'scale_height', 'scale_both', None], doc="""
154

155
        How the component should size itself.
156

157
        This is a high-level setting for maintaining width and height
158
        of the component. To gain more fine grained control over
159
        sizing, use ``width_policy``, ``height_policy`` and
160
        ``aspect_ratio`` instead (those take precedence over
161
        ``sizing_mode``).
162

163
        ``"fixed"``
164
            Component is not responsive. It will retain its original
165
            width and height regardless of any subsequent browser
166
            window resize events.
167

168
        ``"stretch_width"``
169
            Component will responsively resize to stretch to the
170
            available width, without maintaining any aspect ratio. The
171
            height of the component depends on the type of the
172
            component and may be fixed or fit to component's contents.
173

174
        ``"stretch_height"``
175
            Component will responsively resize to stretch to the
176
            available height, without maintaining any aspect
177
            ratio. The width of the component depends on the type of
178
            the component and may be fixed or fit to component's
179
            contents.
180

181
        ``"stretch_both"``
182
            Component is completely responsive, independently in width
183
            and height, and will occupy all the available horizontal
184
            and vertical space, even if this changes the aspect ratio
185
            of the component.
186

187
        ``"scale_width"``
188
            Component will responsively resize to stretch to the
189
            available width, while maintaining the original or
190
            provided aspect ratio.
191

192
        ``"scale_height"``
193
            Component will responsively resize to stretch to the
194
            available height, while maintaining the original or
195
            provided aspect ratio.
196

197
        ``"scale_both"``
198
            Component will responsively resize to both the available
199
            width and height, while maintaining the original or
200
            provided aspect ratio.
201
    """)
202

203 2
    __abstract = True
204

205 2
    def __init__(self, **params):
206 2
        if (params.get('width', None) is not None and
207
            params.get('height', None) is not None and
208
            params.get('width_policy') is None and
209
            params.get('height_policy') is None and
210
            'sizing_mode' not in params):
211 2
            params['sizing_mode'] = 'fixed'
212 2
        elif (not (self.param.sizing_mode.constant or self.param.sizing_mode.readonly) and
213
              type(self).sizing_mode is None):
214 2
            params['sizing_mode'] = params.get('sizing_mode', config.sizing_mode)
215 2
        super(Layoutable, self).__init__(**params)
216

217

218 2
class ServableMixin(object):
219
    """
220
    Mixin to define methods shared by objects which can served.
221
    """
222

223 2
    def _modify_doc(self, server_id, title, doc, location):
224
        """
225
        Callback to handle FunctionHandler document creation.
226
        """
227 2
        if server_id:
228 2
            state._servers[server_id][2].append(doc)
229 2
        return self.server_doc(doc, title, location)
230

231 2
    def _add_location(self, doc, location, root=None):
232 2
        from .io.location import Location
233 2
        if isinstance(location, Location):
234 0
            loc = location
235 2
        elif doc in state._locations:
236 0
            loc = state._locations[doc]
237
        else:
238 2
            loc = Location()
239 2
        state._locations[doc] = loc
240 2
        if root is None:
241 0
            loc_model = loc._get_root(doc)
242
        else:
243 2
            loc_model = loc._get_model(doc, root)
244 2
        loc_model.name = 'location'
245 2
        doc.add_root(loc_model)
246 2
        return loc
247

248 2
    def _on_msg(self, ref, manager, msg):
249
        """
250
        Handles Protocol messages arriving from the client comm.
251
        """
252 0
        root, doc, comm = state._views[ref][1:]
253 0
        patch_cds_msg(root, msg)
254 0
        held = doc._hold
255 0
        patch = manager.assemble(msg)
256 0
        doc.hold()
257 0
        patch.apply_to_document(doc, comm.id)
258 0
        doc.unhold()
259 0
        if held:
260 0
            doc.hold(held)
261

262 2
    def _on_error(self, ref, error):
263 2
        if ref not in state._handles or config.console_output in [None, 'disable']:
264 2
            return
265 2
        handle, accumulator = state._handles[ref]
266 2
        formatted = '\n<pre>'+escape(traceback.format_exc())+'</pre>\n'
267 2
        if config.console_output == 'accumulate':
268 2
            accumulator.append(formatted)
269 2
        elif config.console_output == 'replace':
270 2
            accumulator[:] = [formatted]
271 2
        if accumulator:
272 2
            handle.update({'text/html': '\n'.join(accumulator)}, raw=True)
273

274 2
    def _on_stdout(self, ref, stdout):
275 2
        if ref not in state._handles or config.console_output is [None, 'disable']:
276 0
            return
277 2
        handle, accumulator = state._handles[ref]
278 2
        formatted = ["%s</br>" % o for o in stdout]
279 2
        if config.console_output == 'accumulate':
280 2
            accumulator.extend(formatted)
281 2
        elif config.console_output == 'replace':
282 2
            accumulator[:] = formatted
283 2
        if accumulator:
284 2
            handle.update({'text/html': '\n'.join(accumulator)}, raw=True)
285

286
    #----------------------------------------------------------------
287
    # Public API
288
    #----------------------------------------------------------------
289

290 2
    def servable(self, title=None, location=True):
291
        """
292
        Serves the object if in a `panel serve` context and returns
293
        the Panel object to allow it to display itself in a notebook
294
        context.
295
        Arguments
296
        ---------
297
        title : str
298
          A string title to give the Document (if served as an app)
299
        location : boolean or panel.io.location.Location
300
          Whether to create a Location component to observe and
301
          set the URL location.
302

303
        Returns
304
        -------
305
        The Panel object itself
306
        """
307 0
        if _curdoc().session_context:
308 0
            logger = logging.getLogger('bokeh')
309 0
            for handler in logger.handlers:
310 0
                if isinstance(handler, logging.StreamHandler):
311 0
                    handler.setLevel(logging.WARN)
312 0
            self.server_doc(title=title, location=True)
313 0
        return self
314

315 2
    def show(self, title=None, port=0, address=None, websocket_origin=None,
316
             threaded=False, verbose=True, open=True, location=True, **kwargs):
317
        """
318
        Starts a Bokeh server and displays the Viewable in a new tab.
319

320
        Arguments
321
        ---------
322
        title : str
323
          A string title to give the Document (if served as an app)
324
        port: int (optional, default=0)
325
          Allows specifying a specific port
326
        address : str
327
          The address the server should listen on for HTTP requests.
328
        websocket_origin: str or list(str) (optional)
329
          A list of hosts that can connect to the websocket.
330
          This is typically required when embedding a server app in
331
          an external web site.
332
          If None, "localhost" is used.
333
        threaded: boolean (optional, default=False)
334
          Whether to launch the Server on a separate thread, allowing
335
          interactive use.
336
        verbose: boolean (optional, default=True)
337
          Whether to print the address and port
338
        open : boolean (optional, default=True)
339
          Whether to open the server in a new browser tab
340
        location : boolean or panel.io.location.Location
341
          Whether to create a Location component to observe and
342
          set the URL location.
343

344
        Returns
345
        -------
346
        server: bokeh.server.Server or threading.Thread
347
          Returns the Bokeh server instance or the thread the server
348
          was launched on (if threaded=True)
349
        """
350 0
        return serve(
351
            self, port=port, address=address, websocket_origin=websocket_origin,
352
            show=open, start=True, title=title, verbose=verbose,
353
            location=location, threaded=threaded, **kwargs
354
        )
355

356

357 2
class Renderable(param.Parameterized):
358
    """
359
    Baseclass for objects which can be rendered to a Bokeh model.
360

361
    It therefore declare APIs for initializing the models from
362
    parameter values.
363
    """
364

365 2
    __abstract = True
366

367 2
    def __init__(self, **params):
368 2
        super(Renderable, self).__init__(**params)
369 2
        self._documents = {}
370 2
        self._models = {}
371 2
        self._comms = {}
372 2
        self._kernels = {}
373 2
        self._found_links = set()
374

375 2
    def _get_model(self, doc, root=None, parent=None, comm=None):
376
        """
377
        Converts the objects being wrapped by the viewable into a
378
        bokeh model that can be composed in a bokeh layout.
379

380
        Arguments
381
        ----------
382
        doc: bokeh.Document
383
          Bokeh document the bokeh model will be attached to.
384
        root: bokeh.Model
385
          The root layout the viewable will become part of.
386
        parent: bokeh.Model
387
          The parent layout the viewable will become part of.
388
        comm: pyviz_comms.Comm
389
          Optional pyviz_comms when working in notebook
390

391
        Returns
392
        -------
393
        model: bokeh.Model
394
        """
395 0
        raise NotImplementedError
396

397 2
    def _cleanup(self, root):
398
        """
399
        Clean up method which is called when a Viewable is destroyed.
400

401
        Arguments
402
        ---------
403
        root: bokeh.model.Model
404
          Bokeh model for the view being cleaned up
405
        """
406 2
        ref = root.ref['id']
407 2
        if ref in state._handles:
408 2
            del state._handles[ref]
409

410 2
    def _preprocess(self, root):
411
        """
412
        Applies preprocessing hooks to the model.
413
        """
414 2
        hooks = self._preprocessing_hooks+self._hooks
415 2
        for hook in hooks:
416 2
            hook(self, root)
417

418 2
    def _render_model(self, doc=None, comm=None):
419 0
        if doc is None:
420 0
            doc = _Document()
421 0
        if comm is None:
422 0
            comm = state._comm_manager.get_server_comm()
423 0
        model = self.get_root(doc, comm)
424

425 0
        if config.embed:
426 0
            embed_state(self, model, doc,
427
                        json=config.embed_json,
428
                        json_prefix=config.embed_json_prefix,
429
                        save_path=config.embed_save_path,
430
                        load_path=config.embed_load_path,
431
                        progress=False)
432
        else:
433 0
            add_to_doc(model, doc)
434 0
        return model
435

436 2
    def _init_properties(self):
437 2
        return {k: v for k, v in self.param.get_param_values()
438
                if v is not None}
439

440 2
    def _server_destroy(self, session_context):
441
        """
442
        Server lifecycle hook triggered when session is destroyed.
443
        """
444 2
        doc = session_context._document
445 2
        root = self._documents[doc]
446 2
        ref = root.ref['id']
447 2
        self._cleanup(root)
448 2
        del self._documents[doc]
449 2
        if ref in state._views:
450 2
            del state._views[ref]
451 2
        if doc in state._locations:
452 0
            loc = state._locations[doc]
453 0
            loc._cleanup(root)
454 0
            del state._locations[doc]
455

456 2
    def get_root(self, doc=None, comm=None):
457
        """
458
        Returns the root model and applies pre-processing hooks
459

460
        Arguments
461
        ---------
462
        doc: bokeh.Document
463
          Bokeh document the bokeh model will be attached to.
464
        comm: pyviz_comms.Comm
465
          Optional pyviz_comms when working in notebook
466

467
        Returns
468
        -------
469
        Returns the bokeh model corresponding to this panel object
470
        """
471 2
        doc = doc or _curdoc()
472 2
        root = self._get_model(doc, comm=comm)
473 2
        self._preprocess(root)
474 2
        ref = root.ref['id']
475 2
        state._views[ref] = (self, root, doc, comm)
476 2
        return root
477

478

479 2
class Viewable(Renderable, Layoutable, ServableMixin):
480
    """
481
    Viewable is the baseclass all visual components in the panel
482
    library are built on. It defines the interface for declaring any
483
    object that displays itself by transforming the object(s) being
484
    wrapped into models that can be served using bokeh's layout
485
    engine. The class also defines various methods that allow Viewable
486
    objects to be displayed in the notebook and on bokeh server.
487
    """
488

489 2
    _preprocessing_hooks = []
490

491 2
    def __init__(self, **params):
492 2
        hooks = params.pop('hooks', [])
493 2
        super().__init__(**params)
494 2
        self._hooks = hooks
495

496 2
    def __repr__(self, depth=0):
497 2
        return '{cls}({params})'.format(cls=type(self).__name__,
498
                                        params=', '.join(param_reprs(self)))
499

500 2
    def __str__(self):
501 2
        return self.__repr__()
502

503 2
    def _repr_mimebundle_(self, include=None, exclude=None):
504 2
        loaded = panel_extension._loaded
505 2
        if not loaded and 'holoviews' in sys.modules:
506 2
            import holoviews as hv
507 2
            loaded = hv.extension._loaded
508

509 2
        if config.comms == 'ipywidgets':
510 2
            widget = ipywidget(self)
511 2
            data = {}
512 2
            if widget._view_name is not None:
513 2
                data['application/vnd.jupyter.widget-view+json'] = {
514
                    'version_major': 2,
515
                    'version_minor': 0,
516
                    'model_id': widget._model_id
517
                }
518 2
            if widget._view_name is not None:
519 2
                widget._handle_displayed()
520 2
            return data, {}
521

522 0
        if not loaded:
523 0
            self.param.warning('Displaying Panel objects in the notebook '
524
                               'requires the panel extension to be loaded. '
525
                               'Ensure you run pn.extension() before '
526
                               'displaying objects in the notebook.')
527 0
            return None
528

529 0
        try:
530 0
            from IPython import get_ipython
531 0
            assert get_ipython().kernel is not None
532 0
            state._comm_manager = JupyterCommManager
533 0
        except Exception:
534 0
            pass
535

536 0
        if not state._views:
537
            # Initialize the global Location
538 0
            from .io.location import Location
539 0
            state._location = location = Location()
540
        else:
541 0
            location = None
542

543 0
        from IPython.display import display
544 0
        from .models.comm_manager import CommManager
545

546 0
        doc = _Document()
547 0
        comm = state._comm_manager.get_server_comm()
548 0
        model = self._render_model(doc, comm)
549 0
        ref = model.ref['id']
550 0
        manager = CommManager(comm_id=comm.id, plot_id=ref)
551 0
        client_comm = state._comm_manager.get_client_comm(
552
            on_msg=partial(self._on_msg, ref, manager),
553
            on_error=partial(self._on_error, ref),
554
            on_stdout=partial(self._on_stdout, ref)
555
        )
556 0
        self._comms[ref] = (comm, client_comm)
557 0
        manager.client_comm_id = client_comm.id
558

559 0
        if config.console_output != 'disable':
560 0
            handle = display(display_id=uuid.uuid4().hex)
561 0
            state._handles[ref] = (handle, [])
562

563 0
        if config.embed:
564 0
            return render_model(model)
565 0
        return render_mimebundle(model, doc, comm, manager, location)
566

567
    #----------------------------------------------------------------
568
    # Public API
569
    #----------------------------------------------------------------
570

571 2
    def clone(self, **params):
572
        """
573
        Makes a copy of the object sharing the same parameters.
574

575
        Arguments
576
        ---------
577
        params: Keyword arguments override the parameters on the clone.
578

579
        Returns
580
        -------
581
        Cloned Viewable object
582
        """
583 2
        return type(self)(**dict(self.param.get_param_values(), **params))
584

585 2
    def pprint(self):
586
        """
587
        Prints a compositional repr of the class.
588
        """
589 0
        print(self)
590

591 2
    def select(self, selector=None):
592
        """
593
        Iterates over the Viewable and any potential children in the
594
        applying the Selector.
595

596
        Arguments
597
        ---------
598
        selector: type or callable or None
599
          The selector allows selecting a subset of Viewables by
600
          declaring a type or callable function to filter by.
601

602
        Returns
603
        -------
604
        viewables: list(Viewable)
605
        """
606 2
        if (selector is None or
607
            (isinstance(selector, type) and isinstance(self, selector)) or
608
            (callable(selector) and not isinstance(selector, type) and selector(self))):
609 2
            return [self]
610
        else:
611 2
            return []
612

613 2
    def app(self, notebook_url="localhost:8888", port=0):
614
        """
615
        Displays a bokeh server app inline in the notebook.
616

617
        Arguments
618
        ---------
619
        notebook_url: str
620
          URL to the notebook server
621
        port: int (optional, default=0)
622
          Allows specifying a specific port
623
        """
624 0
        return show_server(self, notebook_url, port)
625

626 2
    def embed(self, max_states=1000, max_opts=3, json=False, json_prefix='',
627
              save_path='./', load_path=None, progress=False, states={}):
628
        """
629
        Renders a static version of a panel in a notebook by evaluating
630
        the set of states defined by the widgets in the model. Note
631
        this will only work well for simple apps with a relatively
632
        small state space.
633

634
        Arguments
635
        ---------
636
        max_states: int
637
          The maximum number of states to embed
638
        max_opts: int
639
          The maximum number of states for a single widget
640
        json: boolean (default=True)
641
          Whether to export the data to json files
642
        json_prefix: str (default='')
643
          Prefix for JSON filename
644
        save_path: str (default='./')
645
          The path to save json files to
646
        load_path: str (default=None)
647
          The path or URL the json files will be loaded from.
648
        progress: boolean (default=False)
649
          Whether to report progress
650
        states: dict (default={})
651
          A dictionary specifying the widget values to embed for each widget
652
        """
653 0
        show_embed(
654
            self, max_states, max_opts, json, json_prefix, save_path,
655
            load_path, progress, states
656
        )
657

658 2
    def save(self, filename, title=None, resources=None, template=None,
659
             template_variables=None, embed=False, max_states=1000,
660
             max_opts=3, embed_json=False, json_prefix='', save_path='./',
661
             load_path=None, embed_states={}):
662
        """
663
        Saves Panel objects to file.
664

665
        Arguments
666
        ---------
667
        filename: string or file-like object
668
           Filename to save the plot to
669
        title: string
670
           Optional title for the plot
671
        resources: bokeh resources
672
           One of the valid bokeh.resources (e.g. CDN or INLINE)
673
        template:
674
           passed to underlying io.save
675
        template_variables:
676
           passed to underlying io.save
677
        embed: bool
678
           Whether the state space should be embedded in the saved file.
679
        max_states: int
680
           The maximum number of states to embed
681
        max_opts: int
682
           The maximum number of states for a single widget
683
        embed_json: boolean (default=True)
684
           Whether to export the data to json files
685
        json_prefix: str (default='')
686
           Prefix for the auto-generated json directory
687
        save_path: str (default='./')
688
           The path to save json files to
689
        load_path: str (default=None)
690
           The path or URL the json files will be loaded from.
691
        embed_states: dict (default={})
692
          A dictionary specifying the widget values to embed for each widget
693
        """
694 2
        return save(self, filename, title, resources, template,
695
                    template_variables, embed, max_states, max_opts,
696
                    embed_json, json_prefix, save_path, load_path,
697
                    embed_states)
698

699 2
    def server_doc(self, doc=None, title=None, location=True):
700
        """
701
        Returns a serveable bokeh Document with the panel attached
702

703
        Arguments
704
        ---------
705
        doc : bokeh.Document (optional)
706
          The bokeh Document to attach the panel to as a root,
707
          defaults to bokeh.io.curdoc()
708
        title : str
709
          A string title to give the Document
710
        location : boolean or panel.io.location.Location
711
          Whether to create a Location component to observe and
712
          set the URL location.
713

714
        Returns
715
        -------
716
        doc : bokeh.Document
717
          The bokeh document the panel was attached to
718
        """
719 2
        doc = doc or _curdoc()
720 2
        title = title or 'Panel Application'
721 2
        doc.title = title
722 2
        model = self.get_root(doc)
723 2
        if hasattr(doc, 'on_session_destroyed'):
724 2
            doc.on_session_destroyed(self._server_destroy)
725 2
            self._documents[doc] = model
726 2
        add_to_doc(model, doc)
727 2
        if location: self._add_location(doc, location, model)
728 2
        return doc

Read our documentation on viewing source code .

Loading