Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve autoreload of external packages #6459

Merged
merged 29 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d8f5c8
Improve autoreload of external packages
philippjfr Mar 7, 2024
3f29fb3
Allow dynamically updating watched modules and reload local modules s…
philippjfr Mar 8, 2024
c18bfa7
Refactor code handlers
philippjfr Mar 9, 2024
85df6c4
More refactoring
philippjfr Mar 9, 2024
04eb414
Fix import
philippjfr Mar 9, 2024
234fe57
Factor out general document initialization into Application
philippjfr Mar 10, 2024
fae54e8
Fixes and tests
philippjfr Mar 10, 2024
574b9ac
Fix autoreload shutdown
philippjfr Mar 10, 2024
1d01627
Fix Jupyter executor
philippjfr Mar 11, 2024
1fc04ca
Add test for local modules
philippjfr Mar 11, 2024
4d0cbe6
Fix test
philippjfr Mar 11, 2024
b96e952
Merge branch 'main' into reload_all_modules
philippjfr Mar 11, 2024
656d37a
Merge branch 'main' into reload_all_modules
philippjfr Mar 11, 2024
8346d72
Use pytest async auto mode
philippjfr Mar 11, 2024
0ed1ef7
Fix test
philippjfr Mar 12, 2024
1cbbae2
Fix unit test
philippjfr Mar 12, 2024
f872ab0
Robustify cleanup
philippjfr Mar 12, 2024
3143fd5
Small fix
philippjfr Mar 12, 2024
6b67c75
Set stop event
philippjfr Mar 12, 2024
731b28e
Make windows tests more robust
philippjfr Mar 12, 2024
c69d85c
Async test cleanup
philippjfr Mar 12, 2024
2dd12b6
Add watchfiles to core test deps
philippjfr Mar 12, 2024
f3f0f61
Merge branch 'main' into reload_all_modules
philippjfr Mar 12, 2024
dd04796
Apply suggestions from code review
philippjfr Mar 12, 2024
74b0def
Update panel/io/server.py
philippjfr Mar 12, 2024
762f7a9
Apply suggestions from code review
philippjfr Mar 12, 2024
64b1627
Apply suggestions from code review
philippjfr Mar 12, 2024
1f2bde5
Fix pre-commit
philippjfr Mar 12, 2024
04bbbf6
Run 3.12 tests in parallel
philippjfr Mar 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,12 +484,11 @@ async def _handle_callback(self, message, loop: asyncio.BaseEventLoop):
response = await self.callback(*callback_args)
elif isasyncgenfunction(self.callback):
response = self.callback(*callback_args)
elif isgeneratorfunction(self.callback):
response = self._to_async_gen(self.callback(*callback_args))
# printing type(response) -> <class 'async_generator'>
else:
if isgeneratorfunction(self.callback):
response = self._to_async_gen(self.callback(*callback_args))
# printing type(response) -> <class 'async_generator'>
else:
response = await asyncio.to_thread(self.callback, *callback_args)
response = await asyncio.to_thread(self.callback, *callback_args)
await self._serialize_response(response)

async def _prepare_response(self, _) -> None:
Expand Down
2 changes: 1 addition & 1 deletion panel/command/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def customize_kwargs(self, args, server_kwargs):
argvs = {f: args.args for f in files}
applications = build_single_handler_applications(files, argvs)
if args.autoreload:
with record_modules():
with record_modules(list(applications.values())):
self.warm_applications(
applications, args.reuse_sessions
)
Expand Down
93 changes: 93 additions & 0 deletions panel/io/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Extensions for Bokeh application handling.
"""
from __future__ import annotations

import logging
import os

from functools import partial
from typing import TYPE_CHECKING

import bokeh.command.util

from bokeh.application import Application as BkApplication
from bokeh.application.handlers.directory import DirectoryHandler

from ..config import config
from .document import _destroy_document
from .handlers import MarkdownHandler, NotebookHandler, ScriptHandler
from .logging import LOG_SESSION_DESTROYED, LOG_SESSION_LAUNCHING
from .state import set_curdoc, state

if TYPE_CHECKING:
from bokeh.application.handlers.handler import Handler

log = logging.getLogger('panel.io.application')


class Application(BkApplication):
"""
Extends Bokeh Application with ability to add global session
creation callbacks, support for the admin dashboard and the
ability to globally define a template.
"""

def __init__(self, *args, **kwargs):
self._admin = kwargs.pop('admin', None)
super().__init__(*args, **kwargs)

async def on_session_created(self, session_context):
with set_curdoc(session_context._document):
if self._admin is not None:
config._admin = self._admin
for cb in state._on_session_created_internal+state._on_session_created:
cb(session_context)
await super().on_session_created(session_context)

def initialize_document(self, doc):
log.info(LOG_SESSION_LAUNCHING, id(doc))
super().initialize_document(doc)
if doc in state._templates and doc not in state._templates[doc]._documents:
template = state._templates[doc]
with set_curdoc(doc):
template.server_doc(title=template.title, location=True, doc=doc)
def _log_session_destroyed(session_context):
log.info(LOG_SESSION_DESTROYED, id(doc))
doc.destroy = partial(_destroy_document, doc) # type: ignore
doc.on_event('document_ready', partial(state._schedule_on_load, doc))
doc.on_session_destroyed(_log_session_destroyed)

bokeh.command.util.Application = Application # type: ignore


def build_single_handler_application(path, argv=None):
argv = argv or []
path = os.path.abspath(os.path.expanduser(path))
handler: Handler

# There are certainly race conditions here if the file/directory is deleted
# in between the isdir/isfile tests and subsequent code. But it would be a
# failure if they were not there to begin with, too (just a different error)
if os.path.isdir(path):
handler = DirectoryHandler(filename=path, argv=argv)
elif os.path.isfile(path):
if path.endswith(".ipynb"):
handler = NotebookHandler(filename=path, argv=argv)
elif path.endswith(".md"):
handler = MarkdownHandler(filename=path, argv=argv)
elif path.endswith(".py"):
handler = ScriptHandler(filename=path, argv=argv)
else:
raise ValueError(f"Expected a '.py' script or '.ipynb' notebook, got: {path!r}" % path)
else:
raise ValueError(f"Path for Bokeh server application does not exist: {path}")

if handler.failed:
raise RuntimeError(f"Error loading {path}:\n\n{handler.error}\n{handler.error_detail} ")

application = Application(handler)

return application

bokeh.command.util.build_single_handler_application = build_single_handler_application
2 changes: 1 addition & 1 deletion panel/io/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from .. import __version__, config
from ..util import base_version, escape
from .handlers import build_single_handler_application
from .application import build_single_handler_application
from .loading import LOADING_INDICATOR_CSS_CLASS
from .mime_render import find_requirements
from .resources import (
Expand Down
40 changes: 40 additions & 0 deletions panel/io/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import asyncio
import dataclasses
import datetime as dt
import gc
import inspect
import json
import logging
import threading
import weakref

from contextlib import contextmanager
from functools import partial, wraps
Expand Down Expand Up @@ -173,6 +175,44 @@ async def _dispatch_msgs(doc, msgs):
await asyncio.sleep(0.01)
_dispatch_write_task(doc, _dispatch_msgs, doc, remaining)

def _destroy_document(self, session):
"""
Override for Document.destroy() without calling gc.collect directly.
The gc.collect() call is scheduled as a task, ensuring that when
multiple documents are destroyed in quick succession we do not
schedule excessive garbage collection.
"""
if session is not None:
self.remove_on_change(session)

del self._roots
del self._theme
del self._template
self._session_context = None

self.callbacks.destroy()
self.models.destroy()
self.modules.destroy()

# Clear periodic callbacks
for cb in state._periodic.get(self, []):
cb.stop()

# Clean up pn.state to avoid tasks getting executed on dead session
for attr in dir(state):
# _param_watchers is deprecated in Param 2.0 and will raise a warning
if not attr.startswith('_') or attr == "_param_watchers":
continue
state_obj = getattr(state, attr)
if isinstance(state_obj, weakref.WeakKeyDictionary) and self in state_obj:
del state_obj[self]

# Schedule GC
at = dt.datetime.now() + dt.timedelta(seconds=5)
state.schedule_task('gc.collect', gc.collect, at=at)

del self.destroy

#---------------------------------------------------------------------
# Public API
#---------------------------------------------------------------------
Expand Down
Loading
Loading