From 396ad6dcdebe727872b5de84b265b67662e1b074 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 16 Nov 2024 23:15:31 +0100 Subject: [PATCH] More types --- panel/command/convert.py | 4 +- panel/custom.py | 46 +++++++------- panel/io/admin.py | 9 ++- panel/io/application.py | 28 +++++---- panel/io/convert.py | 30 ++++----- panel/io/datamodel.py | 5 +- panel/io/document.py | 21 ++----- panel/io/fastapi.py | 10 ++- panel/io/handlers.py | 34 ++++++---- panel/io/jupyter_executor.py | 15 ++--- panel/io/jupyter_server_extension.py | 22 ++++--- panel/io/mime_render.py | 6 +- panel/io/model.py | 4 +- panel/io/notifications.py | 8 ++- panel/io/pyodide.py | 26 ++++---- panel/io/resources.py | 6 +- panel/io/save.py | 3 +- panel/io/server.py | 87 ++++++++++++++------------ panel/io/state.py | 7 ++- panel/layout/base.py | 4 +- panel/layout/grid.py | 11 ++-- panel/layout/gridstack.py | 2 +- panel/links.py | 7 ++- panel/models/ace.py | 4 +- panel/models/deckgl.py | 4 +- panel/models/vega.py | 12 +++- panel/models/vtk.py | 4 +- panel/pane/base.py | 36 +++++------ panel/pane/equation.py | 4 +- panel/pane/image.py | 7 ++- panel/pane/markup.py | 2 +- panel/reactive.py | 90 ++++++++++++++++----------- panel/template/base.py | 2 +- panel/template/react/__init__.py | 4 +- panel/tests/pane/test_vega.py | 2 +- panel/tests/pane/test_vtk.py | 4 +- panel/tests/theme/test_base.py | 2 +- panel/tests/ui/pane/test_ipywidget.py | 4 +- panel/tests/ui/pane/test_textual.py | 2 +- panel/tests/ui/pane/test_vega.py | 2 +- panel/tests/util.py | 8 +-- panel/util/checks.py | 2 +- panel/viewable.py | 81 +++++++++++++++--------- panel/widgets/base.py | 8 +-- panel/widgets/indicators.py | 2 +- panel/widgets/input.py | 6 +- panel/widgets/slider.py | 2 +- pyproject.toml | 1 + 48 files changed, 392 insertions(+), 298 deletions(-) diff --git a/panel/command/convert.py b/panel/command/convert.py index e5fa0785a2..affc02a384 100644 --- a/panel/command/convert.py +++ b/panel/command/convert.py @@ -41,7 +41,7 @@ class Convert(Subcommand): )), ('--compiled', Argument( default = False, - action = 'store_false', + action = 'store_true', help = "Whether to use the compiled and faster version of Pyodide." )), ('--out', Argument( @@ -75,7 +75,7 @@ class Convert(Subcommand): )), ('--disable-http-patch', Argument( default = False, - action = 'store_false', + action = 'store_true', help = "Whether to disable patching http requests using the pyodide-http library." )), ('--watch', Argument( diff --git a/panel/custom.py b/panel/custom.py index 98fcdeafbc..6758121ec8 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: from bokeh.document import Document from bokeh.events import Event - from bokeh.model import Model + from bokeh.model import Model, UIElement from pyviz_comms import Comm ExportSpec = dict[str, list[str | tuple[str, ...]]] @@ -152,7 +152,7 @@ def _get_model( return model def select( - self, selector: Optional[type | Callable[Viewable, bool]] = None + self, selector: type | Callable[[Viewable], bool] | None = None ) -> list[Viewable]: return super().select(selector) + self._view__.select(selector) @@ -250,19 +250,19 @@ def _module_path(cls): @classproperty def _bundle_path(cls) -> os.PathLike | None: if config.autoreload and cls._esm: - return + return None mod_path = cls._module_path if mod_path is None: - return + return None if cls._bundle: for scls in cls.__mro__: if issubclass(scls, ReactiveESM) and cls._bundle == scls._bundle: cls = scls mod_path = cls._module_path bundle = cls._bundle - if isinstance(bundle, pathlib.PurePath): + if isinstance(bundle, os.PathLike): return bundle - elif bundle.endswith('.js'): + elif bundle and bundle.endswith('.js'): bundle_path = mod_path / bundle if bundle_path.is_file(): return bundle_path @@ -286,16 +286,18 @@ def _bundle_path(cls) -> os.PathLike | None: mod = importlib.import_module(submodule) except (ModuleNotFoundError, ImportError): continue - if not hasattr(mod, '__file__'): + mod_file = getattr(mod, '__file__', None) + if not mod_file: continue - submodule_path = pathlib.Path(mod.__file__).parent + submodule_path = pathlib.Path(mod_file).parent path = submodule_path / f'{submodule}.bundle.js' if path.is_file(): return path if module in sys.modules: - module = os.path.basename(sys.modules[module].__file__).replace('.py', '') - path = mod_path / f'{module}.bundle.js' + # Get module name from the module + module_obj = sys.modules[module] + path = mod_path / f'{module_obj.__name__}.bundle.js' return path if path.is_file() else None return None @@ -306,10 +308,10 @@ def _esm_path(cls, compiled: bool = True) -> os.PathLike | None: if bundle_path: return bundle_path esm = cls._esm - if isinstance(esm, pathlib.PurePath): + if isinstance(esm, os.PathLike): return esm - elif not esm.endswith(('.js', '.jsx', '.ts', '.tsx')): - return + elif not esm or not esm.endswith(('.js', '.jsx', '.ts', '.tsx')): + return None try: if hasattr(cls, '__path__'): mod_path = cls.__path__ @@ -320,7 +322,7 @@ def _esm_path(cls, compiled: bool = True) -> os.PathLike | None: return esm_path except (OSError, TypeError, ValueError): pass - return + return None @classmethod def _render_esm(cls, compiled: bool | Literal['compiling'] = True, server: bool = False): @@ -344,7 +346,7 @@ def _render_esm(cls, compiled: bool | Literal['compiling'] = True, server: bool esm = textwrap.dedent(esm) return esm - def _cleanup(self, root: Model | None) -> None: + def _cleanup(self, root: Model | None = None) -> None: if root: ref = root.ref['id'] if ref in self._models: @@ -382,10 +384,10 @@ def _update_esm(self): self._apply_update({}, {'esm': esm}, model, ref) @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return tuple(p for p in self._data_model.properties() if p not in ('js_property_callbacks',)) - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None) -> dict[str, Any]: props = super()._get_properties(doc) cls = type(self) data_params = {} @@ -411,7 +413,7 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: importmap = self._process_importmap() is_session = False if bundle_path: - is_session = (doc.session_context and doc.session_context.server_context) + is_session = bool(doc and doc.session_context and doc.session_context.server_context) if bundle_path == self._esm_path(not config.autoreload) and cls.__module__ in sys.modules and is_session: bundle_hash = 'url' else: @@ -435,7 +437,9 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: def _process_importmap(cls): return cls._importmap - def _get_child_model(self, child, doc, root, parent, comm): + def _get_child_model( + self, child: Viewable, doc: Document, root: Model, parent: Model, comm: Comm | None + ) -> list[UIElement] | UIElement | None: if child is None: return None ref = root.ref['id'] @@ -448,7 +452,7 @@ def _get_child_model(self, child, doc, root, parent, comm): return child._models[ref][0] return child._get_model(doc, root, parent, comm) - def _get_children(self, data_model, doc, root, parent, comm): + def _get_children(self, data_model, doc, root, parent, comm) -> dict[str, list[UIElement] | UIElement | None]: children = {} for k, v in self.param.values().items(): p = self.param[k] @@ -475,7 +479,7 @@ def _get_model( root = root or model children = self._get_children(model.data, doc, root, model, comm) model.data.update(**children) - model.children = list(children) + model.children = list(children) # type: ignore self._models[root.ref['id']] = (model, parent) self._link_props(model.data, self._linked_properties, doc, root, comm) self._register_events('dom_event', 'data_event', model=model, doc=doc, comm=comm) diff --git a/panel/io/admin.py b/panel/io/admin.py index 29c20e6655..9712f69ef4 100644 --- a/panel/io/admin.py +++ b/panel/io/admin.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime as dt import logging import os @@ -5,6 +7,7 @@ import time from functools import partial +from typing import TYPE_CHECKING import bokeh import numpy as np @@ -34,7 +37,11 @@ from .server import set_curdoc from .state import state -PROCESSES = {} +if TYPE_CHECKING: + from psutil import Process + + +PROCESSES: dict[int, Process] = {} log_sessions = [] diff --git a/panel/io/application.py b/panel/io/application.py index d0989bf33a..7621a0ca79 100644 --- a/panel/io/application.py +++ b/panel/io/application.py @@ -10,7 +10,7 @@ from functools import partial from types import FunctionType, MethodType from typing import ( - TYPE_CHECKING, Any, Callable, Mapping, TypeAlias, + TYPE_CHECKING, Any, Callable, Sequence, TypeAlias, ) from urllib.parse import urljoin @@ -48,7 +48,7 @@ def _eval_panel( - panel: TViewableFuncOrPath, server_id: str, title: str, + panel: TViewableFuncOrPath, server_id: str | None, title: str, location: bool | Location, admin: bool, doc: Document ): from ..pane import panel as as_panel @@ -173,7 +173,11 @@ def process_request(self, request) -> dict[str, Any]: if user and config.cookie_secret: from tornado.web import decode_signed_value try: - user = decode_signed_value(config.cookie_secret, 'user', user.value).decode('utf-8') + decoded = decode_signed_value(config.cookie_secret, 'user', user.value) + if decoded: + user = decoded.decode('utf-8') + else: + user = user.value except Exception: user = user.value if user in state._oauth_user_overrides: @@ -186,7 +190,7 @@ def process_request(self, request) -> dict[str, Any]: bokeh.command.util.Application = Application # type: ignore -def build_single_handler_application(path, argv=None): +def build_single_handler_application(path: str | os.PathLike, argv=None) -> Application: argv = argv or [] path = os.path.abspath(os.path.expanduser(path)) handler: Handler @@ -219,13 +223,13 @@ def build_single_handler_application(path, argv=None): def build_applications( - panel: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panel: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], title: str | dict[str, str] | None = None, location: bool | Location = True, admin: bool = False, server_id: str | None = None, - custom_handlers: list | None = None -) -> dict[str, Application]: + custom_handlers: Sequence[Callable[[str, TViewableFuncOrPath], TViewableFuncOrPath]] | None = None +) -> dict[str, BkApplication]: """ Converts a variety of objects into a dictionary of Applications. @@ -248,7 +252,7 @@ def build_applications( if not isinstance(panel, dict): panel = {'/': panel} - apps = {} + apps: dict[str, BkApplication] = {} for slug, app in panel.items(): if slug.endswith('/') and slug != '/': raise ValueError(f"Invalid URL: trailing slash '/' used for {slug!r} not supported.") @@ -260,13 +264,15 @@ def build_applications( "Keys of the title dictionary and of the apps " f"dictionary must match. No {slug} key found in the " "title dictionary.") from None - else: + elif title: title_ = title + else: + title_ = 'Panel Application' slug = slug if slug.startswith('/') else '/'+slug # Handle other types of apps using a custom handler - for handler in (custom_handlers or ()): - new_app = handler(slug, app) + for chandler in (custom_handlers or ()): + new_app = chandler(slug, app) if app is not None: break else: diff --git a/panel/io/convert.py b/panel/io/convert.py index fcb005e83c..31a69a1b04 100644 --- a/panel/io/convert.py +++ b/panel/io/convert.py @@ -58,7 +58,7 @@ PYODIDE_PYC_JS = f'' LOCAL_PREFIX = './' -MINIMUM_VERSIONS = {} +MINIMUM_VERSIONS: dict[str, str] = {} ICON_DIR = DIST_DIR / 'images' PWA_IMAGES = [ @@ -150,7 +150,7 @@ def build_pwa_manifest(files, title=None, **kwargs) -> str: def script_to_html( filename: str | os.PathLike | IO, - requirements: Literal['auto'] | list[str] = 'auto', + requirements: list[str] | Literal['auto'] | os.PathLike = 'auto', js_resources: Literal['auto'] | list[str] = 'auto', css_resources: Literal['auto'] | list[str] | None = 'auto', runtime: Runtimes = 'pyodide', @@ -161,7 +161,7 @@ def script_to_html( http_patch: bool = True, inline: bool = False, compiled: bool = True -) -> str: +) -> tuple[str, str | None]: """ Converts a Panel or Bokeh script to a standalone WASM Python application. @@ -202,7 +202,7 @@ def script_to_html( app_name = '.'.join(path.name.split('.')[:-1]) app = build_single_handler_application(str(path.absolute())) document = Document() - document._session_context = lambda: MockSessionContext(document=document) + document._session_context = lambda: MockSessionContext(document=document) # type: ignore with set_curdoc(document): app.initialize_document(document) state._on_load(None) @@ -416,11 +416,9 @@ def convert_app( with open(dest_path / filename, 'w', encoding="utf-8") as out: out.write(html) - if runtime == 'pyscript-worker': - with open(dest_path / f'{name}.py', 'w', encoding="utf-8") as out: - out.write(worker) - elif runtime == 'pyodide-worker': - with open(dest_path / f'{name}.js', 'w', encoding="utf-8") as out: + if 'worker' in runtime and worker: + ext = 'py' if runtime.startswith('pyscript') else 'js' + with open(dest_path / f'{name}.{ext}', 'w', encoding="utf-8") as out: out.write(worker) if verbose: print(f'Successfully converted {app} to {runtime} target and wrote output to {filename}.') @@ -545,15 +543,19 @@ def convert_apps( app_requirements = requirements kwargs = { - 'runtime': runtime, 'prerender': prerender, - 'manifest': manifest, 'panel_version': panel_version, - 'http_patch': http_patch, 'inline': inline, - 'verbose': verbose, 'compiled': compiled, + 'runtime': runtime, + 'prerender': prerender, + 'manifest': manifest, + 'panel_version': panel_version, + 'http_patch': http_patch, + 'inline': inline, + 'verbose': verbose, + 'compiled': compiled, 'local_prefix': local_prefix } if state._is_pyodide: - files = dict(convert_app(app, dest_path, requirements=app_requirements, **kwargs) for app in apps) + files = {app: convert_app(app, dest_path, requirements=app_requirements, **kwargs) for app in apps} else: files = _convert_process_pool( apps, dest_path, max_workers=max_workers, requirements=app_requirements, **kwargs diff --git a/panel/io/datamodel.py b/panel/io/datamodel.py index b5a080ed03..445408c961 100644 --- a/panel/io/datamodel.py +++ b/panel/io/datamodel.py @@ -1,6 +1,7 @@ -import weakref +from __future__ import annotations from functools import partial +from weakref import WeakKeyDictionary import bokeh import bokeh.core.properties as bp @@ -53,7 +54,7 @@ def validate(self, value, detail=True): raise ValueError(msg) -_DATA_MODELS = weakref.WeakKeyDictionary() +_DATA_MODELS: WeakKeyDictionary[type[pm.Parameterized], type[DataModel]] = WeakKeyDictionary() # The Bokeh Color property has `_default_help` set which causes # an error to be raise when Nullable is called on it. This converter diff --git a/panel/io/document.py b/panel/io/document.py index 5c9fa6aa96..447246b501 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -15,18 +15,13 @@ from contextlib import contextmanager from functools import partial, wraps from typing import ( - TYPE_CHECKING, Any, Callable, Iterator, Sequence, cast, + TYPE_CHECKING, Any, Callable, Iterator, Sequence, ) from weakref import WeakKeyDictionary from bokeh.application.application import SessionContext -from bokeh.core.serialization import Serializable from bokeh.document.document import Document -from bokeh.document.events import ( - ColumnDataChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent, - DocumentChangedEvent, DocumentPatchedEvent, MessageSentEvent, - ModelChangedEvent, -) +from bokeh.document.events import DocumentChangedEvent, DocumentPatchedEvent from bokeh.model.util import visit_immediate_value_references from bokeh.models import CustomJS @@ -52,10 +47,6 @@ # Private API #--------------------------------------------------------------------- -DISPATCH_EVENTS = ( - ColumnDataChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent, - ModelChangedEvent, MessageSentEvent -) GC_DEBOUNCE = 5 _WRITE_FUTURES: WeakKeyDictionary[Document, list[Future]] = WeakKeyDictionary() _WRITE_MSGS: WeakKeyDictionary[Document, dict[ServerConnection, list[Message]]] = WeakKeyDictionary() @@ -502,14 +493,14 @@ def unlocked(policy: HoldPolicyType = 'combine') -> Iterator: curdoc.callbacks._held_events = [] monkeypatch_events(events) for event in events: - if isinstance(event, DISPATCH_EVENTS) and not locked: + if isinstance(event, DocumentPatchedEvent) and not locked: writeable_events.append(event) else: remaining_events.append(event) try: if writeable_events: - write_events(curdoc, connections, cast(list[DocumentPatchedEvent], writeable_events)) + write_events(curdoc, connections, writeable_events) except Exception: remaining_events = events finally: @@ -520,8 +511,8 @@ def unlocked(policy: HoldPolicyType = 'combine') -> Iterator: # the message reflects the event at the time it was generated # potentially avoiding issues serializing subsequent models # which assume the serializer has previously seen them. - serializable_events = [e for e in remaining_events if isinstance(e, Serializable)] - held_events = [e for e in remaining_events if not isinstance(e, Serializable)] + serializable_events = [e for e in remaining_events if isinstance(e, DocumentPatchedEvent)] + held_events = [e for e in remaining_events if not isinstance(e, DocumentPatchedEvent)] if serializable_events: try: schedule_write_events(curdoc, connections, serializable_events) diff --git a/panel/io/fastapi.py b/panel/io/fastapi.py index a18f443278..2b5dc3cd57 100644 --- a/panel/io/fastapi.py +++ b/panel/io/fastapi.py @@ -5,9 +5,7 @@ import uuid from functools import wraps -from typing import ( - TYPE_CHECKING, Any, Mapping, cast, -) +from typing import TYPE_CHECKING, Any, cast from ..config import config from .application import build_applications @@ -78,7 +76,7 @@ async def history_handler(request: Request): #--------------------------------------------------------------------- def add_applications( - panel: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panel: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], app: FastAPI | None = None, title: str | dict[str, str] | None = None, location: bool | Location = True, @@ -189,7 +187,7 @@ def wrapper(*args, **kwargs): def get_server( - panel: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panel: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], port: int | None = 0, show: bool = True, start: bool = False, @@ -288,7 +286,7 @@ def show_callback(): def serve( - panels: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panels: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], port: int = 0, address: str | None = None, websocket_origin: str | list[str] | None = None, diff --git a/panel/io/handlers.py b/panel/io/handlers.py index 6872c0f219..93ff0b6171 100644 --- a/panel/io/handlers.py +++ b/panel/io/handlers.py @@ -14,7 +14,7 @@ from contextlib import contextmanager from types import ModuleType from typing import ( - IO, TYPE_CHECKING, Any, Callable, + IO, TYPE_CHECKING, Any, Callable, Iterator, ) import bokeh.command.util @@ -56,7 +56,7 @@ def _patch_ipython_display(): pass @contextmanager -def _monkeypatch_io(loggers: dict[str, Callable[..., None]]) -> dict[str, Any]: +def _monkeypatch_io(loggers: dict[str, Callable[..., None]]) -> Iterator[None]: import bokeh.io as io old: dict[str, Any] = {} for f in CodeHandler._io_functions: @@ -107,7 +107,7 @@ def extract_code( inblock = False block_opener = None title = None - markdown = [] + markdown: list[str] = [] out = [] while True: line = filehandle.readline() @@ -328,7 +328,7 @@ def parse_notebook( nbconvert = import_required('nbconvert', 'The Panel notebook application handler requires nbconvert to be installed.') nbformat = import_required('nbformat', 'The Panel notebook application handler requires Jupyter Notebook to be installed.') - class StripMagicsProcessor(nbconvert.preprocessors.Preprocessor): + class StripMagicsProcessor(nbconvert.preprocessors.Preprocessor): # type: ignore """ Preprocessor to convert notebooks to Python source while stripping out all magics (i.e IPython specific syntax). @@ -384,8 +384,8 @@ def __call__(self, nb, resources): elif cell['cell_type'] == 'markdown': md = ''.join(cell['source']).replace('"', r'\"') code.append(f'_pn__state._cell_outputs[{cell_id!r}].append("""{md}""")') - code = '\n'.join(code) - return nb, code, cell_layouts + code_string = '\n'.join(code) + return nb, code_string, cell_layouts #--------------------------------------------------------------------- # Handler classes @@ -437,10 +437,22 @@ class PanelCodeHandler(CodeHandler): - Track modules loaded during app execution to enable autoreloading """ - def __init__(self, *, source: str, filename: PathLike, argv: list[str] = [], package: ModuleType | None = None) -> None: + def __init__( + self, + *, + source: str | None = None, + filename: PathLike, argv: list[str] = [], + package: ModuleType | None = None, + runner: PanelCodeRunner | None = None + ) -> None: Handler.__init__(self) - self._runner = PanelCodeRunner(source, filename, argv, package=package) + if runner: + self._runner = runner + elif source: + self._runner = PanelCodeRunner(source, filename, argv, package=package) + else: + raise ValueError("Must provide source code to PanelCodeHandler") self._loggers = {} for f in PanelCodeHandler._io_functions: @@ -476,7 +488,7 @@ def modify_document(self, doc: 'Document'): run_app(self, module, doc) -CodeHandler.modify_document = PanelCodeHandler.modify_document +CodeHandler.modify_document = PanelCodeHandler.modify_document # type: ignore class ScriptHandler(PanelCodeHandler): @@ -500,7 +512,7 @@ def __init__(self, *, filename: PathLike, argv: list[str] = [], package: ModuleT super().__init__(source=source, filename=filename, argv=argv, package=package) -bokeh.application.handlers.directory.ScriptHandler = ScriptHandler +bokeh.application.handlers.directory.ScriptHandler = ScriptHandler # type: ignore class MarkdownHandler(PanelCodeHandler): @@ -728,4 +740,4 @@ def _update_position_metadata(self, event): json.dump(nb_layout, f) self._stale = True -bokeh.application.handlers.directory.NotebookHandler = NotebookHandler +bokeh.application.handlers.directory.NotebookHandler = NotebookHandler # type: ignore diff --git a/panel/io/jupyter_executor.py b/panel/io/jupyter_executor.py index d8c9758f3e..4eea7b2f79 100644 --- a/panel/io/jupyter_executor.py +++ b/panel/io/jupyter_executor.py @@ -10,6 +10,7 @@ import tornado +from bokeh.application import ServerContext from bokeh.document import Document from bokeh.embed.bundle import extension_dirs from bokeh.protocol import Protocol @@ -51,7 +52,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): class JupyterServerSession(ServerSession): - _tasks = set() + _tasks: set[asyncio.Task] = set() def _document_patched(self, event: DocumentPatchedEvent) -> None: may_suppress = event.setter is self @@ -71,7 +72,7 @@ class PanelExecutor(WSHandler): to send and receive messages to and from the frontend. """ - _tasks = set() + _tasks: set[asyncio.Task] = set() def __init__(self, path, token, root_url, resources='server'): self.path = path @@ -144,15 +145,15 @@ def _internal_error(self, msg: str) -> None: def _protocol_error(self, msg: str) -> None: self.comm.send(msg, {'status': 'protocol_error'}) - def _create_server_session(self) -> ServerSession: + def _create_server_session(self) -> tuple[ServerSession, str | None]: doc = Document() self._context = session_context = BokehSessionContext( - self.session_id, None, doc + self.session_id, ServerContext(), doc ) # using private attr so users only have access to a read-only property - session_context._request = _RequestProxy( + session_context._request = _RequestProxy( # type: ignore arguments={k: [v.encode('utf-8') for v in vs] for k, vs in self.payload.get('arguments', {}).items()}, cookies=self.payload.get('cookies'), headers=self.payload.get('headers') @@ -180,7 +181,7 @@ def _create_server_session(self) -> ServerSession: session_context._set_session(session) return session, runner.error_detail - async def write_message( + async def write_message( # type: ignore self, message: Union[bytes, str, dict[str, Any]], binary: bool = False, locked: bool = True ) -> None: @@ -197,7 +198,7 @@ async def write_message( else: self.comm.send(message, metadata=metadata) - def render(self) -> Mimebundle: + def render_mime(self) -> Mimebundle: """ Renders the application to an IPython.display.HTML object to be served by the `PanelJupyterHandler`. diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py index 1c8a7a1f5a..235984f9b7 100644 --- a/panel/io/jupyter_server_extension.py +++ b/panel/io/jupyter_server_extension.py @@ -118,7 +118,7 @@ def get_server_root_dir(settings): from panel.io.jupyter_executor import PanelExecutor executor = PanelExecutor(app, '{{ token }}', '{{ root_url }}') -executor.render() +executor.render_mime() """ def generate_executor(path: str, token: str, root_url: str) -> str: @@ -321,19 +321,23 @@ async def _check_connected(self): await self.kernel_manager.shutdown_kernel(kernel_id, now=True) -class PanelWSProxy(WSHandler, JupyterHandler): +class PanelWSProxy(WSHandler, JupyterHandler): # type: ignore """ The PanelWSProxy serves as a proxy between the frontend and the Jupyter kernel that is running the Panel application. It send and receives Bokeh protocol messages via a Jupyter Comm. """ - _tasks = set() + _tasks: set[asyncio.Task] = set() def __init__(self, tornado_app, *args, **kw) -> None: # Note: tornado_app is stored as self.application kw['application_context'] = None super().__init__(tornado_app, *args, **kw) + self.kernel: Any = None + self.comm_id: str | None = None + self.kernel_id: str | None = None + self.session_id: str | None = None def initialize(self, *args, **kwargs): self._ping_count = 0 @@ -346,10 +350,10 @@ def _keep_alive(self): async def prepare(self): pass - def get_current_user(self): + def get_current_user(self) -> str: return "default_user" - def check_origin(self, origin: str) -> bool: + def check_origin(self, origin_to_satisfy_tornado: str | None = None) -> bool: return True @tornado.web.authenticated @@ -392,7 +396,7 @@ async def open(self, path, *args, **kwargs) -> None: msg = f"Session ID '{self.session_id}' does not correspond to any active kernel." raise RuntimeError(msg) - kernel_info = state._kernels[self.session_id] + kernel_info: tuple[Any, str, str, bool] = state._kernels[self.session_id] self.kernel, self.comm_id, self.kernel_id, _ = kernel_info state._kernels[self.session_id] = kernel_info[:-1] + (True,) @@ -440,7 +444,11 @@ async def _check_for_message(self): await self.send_message(message) async def on_message(self, fragment: str | bytes) -> None: - content = dict(data=fragment, comm_id=self.comm_id, target_name=self.session_id) + content = { + 'comm_id': self.comm_id, + 'data': fragment, + 'target_name': self.session_id + } msg = self.kernel.session.msg("comm_msg", content) self.kernel.shell_channel.send(msg) diff --git a/panel/io/mime_render.py b/panel/io/mime_render.py index f93d70078b..1b31d56342 100644 --- a/panel/io/mime_render.py +++ b/panel/io/mime_render.py @@ -23,7 +23,7 @@ from contextlib import redirect_stderr, redirect_stdout from html import escape from textwrap import dedent -from typing import Any +from typing import IO, Any #--------------------------------------------------------------------- # Import API @@ -123,8 +123,8 @@ def _display(*objs, **kwargs): def exec_with_return( code: str, global_context: dict[str, Any] | None = None, - stdout: Any = None, - stderr: Any = None + stdout: IO | None = None, + stderr: IO | None = None ) -> Any: """ Executes a code snippet and returns the resulting output of the diff --git a/panel/io/model.py b/panel/io/model.py index f227c15522..a75a00ed38 100644 --- a/panel/io/model.py +++ b/panel/io/model.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from typing import ( - TYPE_CHECKING, Any, Iterable, Optional, + TYPE_CHECKING, Any, Iterable, Optional, Sequence, ) import numpy as np @@ -46,7 +46,7 @@ def __eq__(self, other: Any) -> bool: def __ne__(self, other: Any) -> bool: return not np.array_equal(self, other, equal_nan=True) -def monkeypatch_events(events: list[DocumentPatchedEvent]) -> None: +def monkeypatch_events(events: Sequence[DocumentChangedEvent]) -> None: """ Patch events applies patches to events that are to be dispatched avoiding various issues in Bokeh. diff --git a/panel/io/notifications.py b/panel/io/notifications.py index 11771f30c2..abbd0cfb62 100644 --- a/panel/io/notifications.py +++ b/panel/io/notifications.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import param @@ -10,6 +10,7 @@ from ..reactive import ReactiveHTML from ..util import classproperty from .datamodel import _DATA_MODELS, construct_data_model +from .document import create_doc_if_none_exists from .resources import CSS_URLS, bundled_files, get_dist_path from .state import state @@ -78,9 +79,10 @@ def __init__(self, **params): self._notification_watchers = {} def get_root( - self, doc: Optional[Document] = None, comm: Optional[Comm] = None, + self, doc: Document | None = None, comm: Comm | None = None, preprocess: bool = True - ) -> 'Model': + ) -> Model: + doc = create_doc_if_none_exists(doc) root = super().get_root(doc, comm, preprocess) for event, notification in self.js_events.items(): doc.js_on_event(event, CustomJS(code=f""" diff --git a/panel/io/pyodide.py b/panel/io/pyodide.py index 4cbbd4c6ca..74beb66c2c 100644 --- a/panel/io/pyodide.py +++ b/panel/io/pyodide.py @@ -8,7 +8,7 @@ import sys import uuid -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import bokeh import js @@ -40,6 +40,9 @@ resources.RESOURCE_MODE = 'CDN' os.environ['BOKEH_RESOURCES'] = 'cdn' +if TYPE_CHECKING: + from bokeh.core.types import ID + try: from js import document as js_document # noqa try: @@ -106,7 +109,7 @@ def _read_file(*args, **kwargs): def _read_csv(*args, **kwargs): args, kwargs = _read_file(*args, **kwargs) return _read_csv_original(*args, **kwargs) - pandas.read_csv = _read_csv + pandas.read_csv = _read_csv # type: ignore # Patch pandas.read_json _read_json_original = pandas.read_json @@ -114,7 +117,7 @@ def _read_csv(*args, **kwargs): def _read_json(*args, **kwargs): args, kwargs = _read_file(*args, **kwargs) return _read_json_original(*args, **kwargs) - pandas.read_json = _read_json + pandas.read_json = _read_json # type: ignore _tasks = set() @@ -394,7 +397,7 @@ def fetch_binary(url): xhr.send() return io.BytesIO(xhr.response.to_py().tobytes()) -def render_script(obj: Any, target: str) -> str: +def render_script(obj: Any, target: ID) -> str: """ Generates a script to render the supplied object to the target. @@ -437,7 +440,7 @@ def init_doc() -> None: doc = Document() set_curdoc(doc) doc.hold() - doc._session_context = lambda: MockSessionContext(document=doc) + doc._session_context = lambda: MockSessionContext(document=doc) # type: ignore state.curdoc = doc async def show(obj: Any, target: str) -> None: @@ -520,7 +523,9 @@ async def write_doc(doc: Document | None = None) -> tuple[str, str, str]: render_items: str root_ids: str """ - pydoc: Document = doc or state.curdoc + pydoc: Document | None = doc or state.curdoc + if not pydoc: + raise ValueError('Cannot write contents of non-existent Document.') if pydoc in state._templates and pydoc not in state._templates[pydoc]._documents: template = state._templates[pydoc] template.server_doc(title=template.title, location=True, doc=pydoc) @@ -573,12 +578,9 @@ def pyrender( from ..param import ReactiveExpr from ..viewable import Viewable, Viewer PANES = (HoloViews, Interactive, ReactiveExpr) - kwargs = {} - if stdout_callback: - kwargs['stdout'] = WriteCallbackStream(stdout_callback) - if stderr_callback: - kwargs['stderr'] = WriteCallbackStream(stderr_callback) - out = exec_with_return(code, **kwargs) + stdout = WriteCallbackStream(stdout_callback) if stdout_callback else None + stderr = WriteCallbackStream(stderr_callback) if stderr_callback else None + out = exec_with_return(code, stdout=stdout, stderr=stderr) ret = {} if isinstance(out, (Model, Viewable, Viewer)) or any(pane.applies(out) for pane in PANES): doc, model_json = _model_json(as_panel(out), target) diff --git a/panel/io/resources.py b/panel/io/resources.py index 32d9634b88..effad8cc92 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -18,7 +18,9 @@ from contextlib import contextmanager from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import ( + TYPE_CHECKING, ClassVar, Literal, TypedDict, +) import bokeh.embed.wrappers import param @@ -473,7 +475,7 @@ class ResourceComponent: that have to be resolved. """ - _resources: ResourcesType = { + _resources: ClassVar[ResourcesType] = { 'css': {}, 'font': {}, 'js': {}, diff --git a/panel/io/save.py b/panel/io/save.py index 9a095ae80b..f06ca69da7 100644 --- a/panel/io/save.py +++ b/panel/io/save.py @@ -18,7 +18,8 @@ OutputDocumentFor, standalone_docs_json_and_render_items, ) from bokeh.io.export import get_screenshot_as_png -from bokeh.model import Model, UIElement +from bokeh.model import Model +from bokeh.models import UIElement from bokeh.resources import CDN, INLINE, Resources as BkResources from pyviz_comms import Comm diff --git a/panel/io/server.py b/panel/io/server.py index 7e48d13ec2..1e73976fa6 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -76,10 +76,10 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: + from bokeh.application.application import SessionContext from bokeh.bundle import Bundle from bokeh.core.types import ID from bokeh.document.document import DocJson - from bokeh.server.contexts import BokehSessionContext from bokeh.server.session import ServerSession from jinja2 import Template @@ -143,7 +143,7 @@ async def wrapped(*args, **kw): param.parameterized.async_executor = async_execute -def _initialize_session_info(session_context: BokehSessionContext): +def _initialize_session_info(session_context: SessionContext): from ..config import config session_id = session_context.id sessions = state.session_info['sessions'] @@ -156,12 +156,14 @@ def _initialize_session_info(session_context: BokehSessionContext): old_history = list(sessions.items()) sessions = dict(old_history[-(history-1):]) state.session_info['sessions'] = sessions + request = session_context.request + user_agent = request.headers.get('User-Agent') if request else None sessions[session_id] = { 'launched': dt.datetime.now().timestamp(), 'started': None, 'rendered': None, 'ended': None, - 'user_agent': session_context.request.headers.get('User-Agent') + 'user_agent': user_agent } state.param.trigger('session_info') @@ -226,11 +228,13 @@ def html_page_for_render_items( context["roots"] = context["doc"].roots if template is None: - template = BASE_TEMPLATE + tmpl = BASE_TEMPLATE elif isinstance(template, str): - template = _env.from_string("{% extends base %}\n" + template) + tmpl = _env.from_string("{% extends base %}\n" + template) + else: + tmpl = template - html = template.render(context) + html = tmpl.render(context) return html def server_html_page_for_session( @@ -327,7 +331,7 @@ async def stop_autoreload(): if state._admin_context: state._admin_context.run_unload_hook() -bokeh.server.server.Server = Server +bokeh.server.server.Server = Server # type: ignore class LoginUrlMixin: """ @@ -349,7 +353,7 @@ def get_login_url(self): class DocHandler(LoginUrlMixin, BkDocHandler): - @authenticated + @authenticated # type: ignore async def get_session(self) -> ServerSession: from ..config import config path = self.request.path @@ -358,7 +362,7 @@ async def get_session(self) -> ServerSession: key = state._session_key_funcs[path](self.request) session = state._sessions.get(key) if session is None: - session = await super().get_session() + session = await super().get_session() # type: ignore with set_curdoc(session.document): if config.reuse_sessions: key_func = config.session_key_func or (lambda r: (r.path, r.arguments.get('theme', [b'default'])[0].decode('utf-8'))) @@ -394,7 +398,7 @@ def _generate_token_payload(self) -> TokenPayload: arguments = {} if self.request.arguments is None else self.request.arguments payload: TokenPayload = {'headers': headers, 'cookies': cookies, 'arguments': arguments} - payload.update(self.application_context.application.process_request(self.request)) + payload.update(self.application_context.application.process_request(self.request)) # type: ignore return payload def _authorize(self, session: bool = False) -> tuple[bool, str | None]: @@ -407,6 +411,7 @@ def _authorize(self, session: bool = False) -> tuple[bool, str | None]: return True, None authorized = False auth_params = inspect.signature(auth_cb).parameters + auth_args: tuple[dict[str, Any] | None] | tuple[dict[str, Any] | None, str] if len(auth_params) == 1: auth_args = (state.user_info,) elif len(auth_params) == 2: @@ -417,7 +422,7 @@ def _authorize(self, session: bool = False) -> tuple[bool, str | None]: 'which is the user name or 2) two arguments which includes the ' 'user name and the url path the user is trying to access.' ) - auth_error = f'{state.user} is not authorized to access this application.' + auth_error: str | None = f'{state.user} is not authorized to access this application.' try: authorized = auth_cb(*auth_args) if isinstance(authorized, str): @@ -530,7 +535,7 @@ async def get(self, *args, **kwargs) -> None: else: server_url = None - session = await self.get_session() + session = await self.get_session() # type: ignore with set_curdoc(session.document): resources = Resources.from_bokeh( self.application.resources(server_url), absolute=True @@ -556,7 +561,7 @@ def render(self, *args, **kwargs): return super().render(*args, **kwargs) toplevel_patterns[0] = (r'/?', RootHandler) -bokeh.server.tornado.RootHandler = RootHandler +bokeh.server.tornado.RootHandler = RootHandler # type: ignore # Copied from bokeh 2.4.0, to fix directly in bokeh at some point. def create_static_handler(prefix, key, app): @@ -607,7 +612,7 @@ class ComponentResourceHandler(StaticFileHandler): '_css', '_js', 'base_css', 'css', '_stylesheets', 'modifiers', '_bundle_path' ] - def initialize(self, path: Optional[str] = None, default_filename: Optional[str] = None): + def initialize(self, path: str, default_filename: str | None = None): self.root = path self.default_filename = default_filename @@ -685,7 +690,7 @@ def validate_absolute_path(self, root: str, absolute_path: str) -> str: def serve( - panels: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panels: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], port: int = 0, address: Optional[str] = None, websocket_origin: Optional[str | list[str]] = None, @@ -810,11 +815,11 @@ def get_static_routes(static_dirs): return patterns def get_server( - panel: TViewableFuncOrPath | Mapping[str, TViewableFuncOrPath], + panel: TViewableFuncOrPath | dict[str, TViewableFuncOrPath], port: int = 0, - address: Optional[str] = None, - websocket_origin: Optional[str | list[str]] = None, - loop: Optional[IOLoop] = None, + address: str | None = None, + websocket_origin: str | list[str] | None = None, + loop: IOLoop | None = None, show: bool = False, start: bool = False, title: str | dict[str, str] | None = None, @@ -822,24 +827,24 @@ def get_server( location: bool | Location = True, admin: bool = False, static_dirs: Mapping[str, str] = {}, - basic_auth: str = None, - oauth_provider: Optional[str] = None, - oauth_key: Optional[str] = None, - oauth_secret: Optional[str] = None, - oauth_redirect_uri: Optional[str] = None, + basic_auth: str | None = None, + oauth_provider: str | None = None, + oauth_key: str | None = None, + oauth_secret: str | None = None, + oauth_redirect_uri: str | None = None, oauth_extra_params: Mapping[str, str] = {}, - oauth_error_template: Optional[str] = None, - cookie_secret: Optional[str] = None, - oauth_encryption_key: Optional[str] = None, - oauth_jwt_user: Optional[str] = None, - oauth_refresh_tokens: Optional[bool] = None, - oauth_guest_endpoints: Optional[list[str]] = None, - oauth_optional: Optional[bool] = None, - login_endpoint: Optional[str] = None, - logout_endpoint: Optional[str] = None, - login_template: Optional[str] = None, - logout_template: Optional[str] = None, - session_history: Optional[int] = None, + oauth_error_template: str | None = None, + cookie_secret: str | None = None, + oauth_encryption_key: str | None = None, + oauth_jwt_user: str | None = None, + oauth_refresh_tokens: str | None = None, + oauth_guest_endpoints: list[str] | None = None, + oauth_optional: bool | None = None, + login_endpoint: str | None = None, + logout_endpoint: str | None = None, + login_template: str | None = None, + logout_template: str | None = None, + session_history: str | None = None, liveness: bool | str = False, warm: bool = False, **kwargs @@ -1022,7 +1027,7 @@ def flask_handler(slug, app): server_config['basic_auth'] = basic_auth provider = BasicAuthProvider else: - config.oauth_provider = oauth_provider + config.oauth_provider = oauth_provider # type: ignore provider = OAuthProvider opts['auth_provider'] = provider( login_endpoint=login_endpoint, @@ -1043,13 +1048,13 @@ def flask_handler(slug, app): if oauth_redirect_uri: config.oauth_redirect_uri = oauth_redirect_uri # type: ignore if oauth_refresh_tokens is not None: - config.oauth_refresh_tokens = oauth_refresh_tokens + config.oauth_refresh_tokens = oauth_refresh_tokens # type: ignore if oauth_optional is not None: - config.oauth_optional = oauth_optional + config.oauth_optional = oauth_optional # type: ignore if oauth_guest_endpoints is not None: - config.oauth_guest_endpoints = oauth_guest_endpoints + config.oauth_guest_endpoints = oauth_guest_endpoints # type: ignore if oauth_jwt_user is not None: - config.oauth_jwt_user = oauth_jwt_user + config.oauth_jwt_user = oauth_jwt_user # type: ignore opts['cookie_secret'] = config.cookie_secret server = Server(apps, port=port, **opts) diff --git a/panel/io/state.py b/panel/io/state.py index 20a69c3503..978d4bc23f 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -54,6 +54,7 @@ from ..template.base import BaseTemplate from ..viewable import Viewable from ..widgets.indicators import BooleanIndicator + from .application import TViewableFuncOrPath from .browser import BrowserInfo from .cache import _Stack from .callbacks import PeriodicCallback @@ -144,7 +145,7 @@ class _state(param.Parameterized): # Jupyter communication _comm_manager: ClassVar[type[_CommManager]] = _CommManager - _jupyter_kernel_context: ClassVar[BokehSessionContext | None] = None + _jupyter_kernel_context: BokehSessionContext | None = None _kernels: ClassVar[dict[str, tuple[Any, str, str, bool]]] = {} _ipykernels: ClassVar[WeakKeyDictionary[Document, Any]] = WeakKeyDictionary() @@ -171,7 +172,7 @@ class _state(param.Parameterized): _fake_roots: ClassVar[list[str]] = [] # An index of all currently active servers - _servers: ClassVar[dict[str, tuple[Server, Viewable | BaseTemplate, list[Document]]]] = {} + _servers: ClassVar[dict[str, tuple[Server, TViewableFuncOrPath | dict[str, TViewableFuncOrPath], list[Document]]]] = {} _threads: ClassVar[dict[str, StoppableThread]] = {} _server_config: ClassVar[WeakKeyDictionary[Any, dict[str, Any]]] = WeakKeyDictionary() @@ -216,7 +217,7 @@ class _state(param.Parameterized): # Sessions _sessions: ClassVar[dict[Hashable, ServerSession]] = {} - _session_key_funcs: ClassVar[dict[str, Callable[[Any], None]]] = {} + _session_key_funcs: ClassVar[dict[str, Callable[[Any], Any]]] = {} # Layout editor _cell_outputs: ClassVar[defaultdict[Hashable, list[Any]]] = defaultdict(list) diff --git a/panel/layout/base.py b/panel/layout/base.py index 272f641d06..2a1dc20110 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -566,7 +566,7 @@ def __init__(self, *items: list[Any | tuple[str, Any]], **params: Any): params['objects'], names = self._to_objects_and_names(items) super().__init__(**params) self._names = names - self._panels = defaultdict(dict) + self._panels: defaultdict[str, dict[int, Viewable]] = defaultdict(dict) self.param.watch(self._update_names, 'objects') # ALERT: Ensure that name update happens first, should be # replaced by watch precedence support in param @@ -819,7 +819,7 @@ class ListPanel(ListLike, Panel): __abstract = True @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return tuple( self._property_mapping.get(p, p) for p in self.param if p not in ListPanel.param and self._property_mapping.get(p, p) is not None diff --git a/panel/layout/grid.py b/panel/layout/grid.py index 0c1e9675e1..345912bae0 100644 --- a/panel/layout/grid.py +++ b/panel/layout/grid.py @@ -54,7 +54,7 @@ class GridBox(ListPanel): ncols = param.Integer(default=None, bounds=(0, None), doc=""" Number of columns to reflow the layout into.""") - _bokeh_model: ClassVar[Model] = BkGridBox + _bokeh_model: ClassVar[type[Model]] = BkGridBox _linked_properties: ClassVar[tuple[str,...]] = () @@ -203,14 +203,15 @@ def _update_model( msg = dict(msg) preprocess = any(self._rename.get(k, k) in self._preprocess_params for k in msg) update_children = self._rename['objects'] in msg - if update_children or 'ncols' in msg or 'nrows' in msg: + child_name = self._rename['objects'] + if (update_children or 'ncols' in msg or 'nrows' in msg) and child_name: if 'objects' in events: old = events['objects'].old else: old = self.objects objects, old_models = self._get_objects(model, old, doc, root, comm) children = self._get_children(objects, self.nrows, self.ncols) - msg[self._rename['objects']] = children + msg[child_name] = children else: old_models = None @@ -269,9 +270,9 @@ class GridSpec(Panel): nrows = param.Integer(default=None, bounds=(0, None), doc=""" Limits the number of rows that can be assigned.""") - _bokeh_model: ClassVar[Model] = BkGridBox + _bokeh_model: ClassVar[type[Model]] = BkGridBox - _linked_properties: ClassVar[tuple[str]] = () + _linked_properties: tuple[str, ...] = () _rename: ClassVar[Mapping[str, str | None]] = { 'objects': 'children', 'mode': None, 'ncols': None, 'nrows': None diff --git a/panel/layout/gridstack.py b/panel/layout/gridstack.py index 753bb70732..cfcfc7059b 100644 --- a/panel/layout/gridstack.py +++ b/panel/layout/gridstack.py @@ -11,7 +11,7 @@ from .grid import GridSpec -class GridStack(ReactiveHTML, GridSpec): +class GridStack(ReactiveHTML, GridSpec): # type: ignore[misc] """ The `GridStack` layout allows arranging multiple Panel objects in a grid using a simple API to assign objects to individual grid cells or to a grid diff --git a/panel/links.py b/panel/links.py index 50570d0765..2130cb0ca3 100644 --- a/panel/links.py +++ b/panel/links.py @@ -287,7 +287,7 @@ class Link(Callback): # Whether the link requires a target _requires_target = True - def __init__(self, source: Reactive | None, target: JSLinkTarget | None = None, **params): + def __init__(self, source: Reactive, target: JSLinkTarget | None = None, **params): if self._requires_target and target is None: raise ValueError(f'{type(self).__name__} must define a target.') # Source is stored as a weakref to allow it to be garbage collected @@ -579,6 +579,7 @@ def _get_specs( ) -> Sequence[tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: for spec in link.code: src_specs = spec.split('.') + src_spec: tuple[str | None, str] if spec.startswith('event:'): src_spec = (None, spec) elif len(src_specs) > 1: @@ -735,10 +736,10 @@ def _get_code( if isinstance(target, Reactive): tgt_reverse = {v: k for k, v in target._rename.items()} tgt_param = tgt_reverse.get(tgt_spec, tgt_spec) - if tgt_param is None: + if tgt_param is None or tgt_param not in target._target_transforms: tgt_transform = 'value' else: - tgt_transform = target._target_transforms.get(tgt_param, 'value') + tgt_transform = target._target_transforms['value'] or 'value' else: tgt_transform = 'value' if tgt_spec == 'loading': diff --git a/panel/models/ace.py b/panel/models/ace.py index b898a6b363..0943f0b0ef 100644 --- a/panel/models/ace.py +++ b/panel/models/ace.py @@ -66,6 +66,6 @@ def __js_skip__(cls): print_margin = Bool(default=False) - height = Override(default=300) + height = Override(default=300) # type: ignore - width = Override(default=300) + width = Override(default=300) # type: ignore diff --git a/panel/models/deckgl.py b/panel/models/deckgl.py index bb14f882c3..b1b10c4d5a 100644 --- a/panel/models/deckgl.py +++ b/panel/models/deckgl.py @@ -95,6 +95,6 @@ def __js_skip__(cls): throttle = Dict(String, Int) - height = Override(default=400) + height = Override(default=400) # type: ignore - width = Override(default=600) + width = Override(default=600) # type: ignore diff --git a/panel/models/vega.py b/panel/models/vega.py index e08f095f39..a12dbd3dd6 100644 --- a/panel/models/vega.py +++ b/panel/models/vega.py @@ -1,8 +1,9 @@ """ Defines custom VegaPlot bokeh model to render Vega json plots. """ +from bokeh.core.enums import enumeration from bokeh.core.properties import ( - Any, Bool, Dict, Enum, Instance, Int, List, Nullable, String, + Any, Bool, Dict, Enum, Instance, Int, List, Literal, Nullable, String, ) from bokeh.events import ModelEvent from bokeh.models import ColumnDataSource, LayoutDOM @@ -11,6 +12,12 @@ from ..io.resources import bundled_files from ..util import classproperty +VegaThemeType = Literal[ + 'excel', 'ggplot2', 'quartz', 'vox', + 'fivethirtyeight', 'dark', 'latimes', + 'urbaninstitute', 'googlecharts' +] +VegaTheme = enumeration(VegaThemeType) class VegaEvent(ModelEvent): @@ -62,7 +69,6 @@ def __js_skip__(cls): show_actions = Bool(False) - theme = Nullable(Enum('excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', - 'latimes', 'urbaninstitute', 'googlecharts', default=None)) + theme = Nullable(Enum(VegaTheme)) throttle = Dict(String, Int) diff --git a/panel/models/vtk.py b/panel/models/vtk.py index 69f22321eb..c3ae8599c3 100644 --- a/panel/models/vtk.py +++ b/panel/models/vtk.py @@ -71,13 +71,13 @@ def __js_skip__(cls): color_mappers = List(Instance(ColorMapper)) - height = Override(default=300) + height = Override(default=300) # type: ignore orientation_widget = Bool(default=False) interactive_orientation_widget = Bool(default=False) - width = Override(default=300) + width = Override(default=300) # type: ignore annotations = List(Dict(String, Any)) diff --git a/panel/pane/base.py b/panel/pane/base.py index 7a7c149633..f79a8f2355 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -40,7 +40,7 @@ from bokeh.model import Model from pyviz_comms import Comm -def panel(obj: Any, **kwargs) -> Viewable: +def panel(obj: Any, **kwargs) -> Viewable | ServableMixin: """ Creates a displayable Panel object given any valid Python object. @@ -317,7 +317,7 @@ def __init__(self, object=None, **params): #---------------------------------------------------------------- @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return tuple( self._property_mapping.get(p, p) for p in self.param if p not in PaneBase.param and self._property_mapping.get(p, p) is not None @@ -366,14 +366,14 @@ def _update_object( try: if isinstance(parent, _BkGridBox): - indexes = [ - i for i, child in enumerate(parent.children) - if child[0] is old_model - ] + indexes: list[int] = [] + for i, child in enumerate(parent.children): # type: ignore + if child[0] is old_model: + indexes.append(i) if indexes: index = indexes[0] - new_model = (new_model,) + parent.children[index][1:] - parent.children[index] = new_model + new_model = (new_model,) + parent.children[index][1:] # type: ignore + parent.children[index] = new_model # type: ignore else: raise ValueError elif isinstance(parent, _BkReactiveHTML): @@ -382,13 +382,13 @@ def _update_object( index = children.index(old_model) new_models = list(children) new_models[index] = new_model - parent.children[node] = new_models + parent.children[node] = new_models # type: ignore break elif isinstance(parent, _BkTabs): index = [tab.child for tab in parent.tabs].index(old_model) - old_tab = parent.tabs[index] + old_tab = parent.tabs[index] # type: ignore props = dict(old_tab.properties_with_values(), child=new_model) - parent.tabs[index] = _BkTabPanel(**props) + parent.tabs[index] = _BkTabPanel(**props) # type: ignore else: index = parent.children.index(old_model) parent.children[index] = new_model @@ -414,7 +414,7 @@ def _update_object( )) # If there is a fake root we run pre-processors on it - if fake_view is not None and view in fake_view: + if fake_view is not None and view in fake_view and fake_root: fake_view._preprocess(fake_root, self) else: view._preprocess(root, self) @@ -445,7 +445,7 @@ def _update(self, ref: str, model: Model) -> None: raise NotImplementedError def _get_root_model( - self, doc: Optional[Document] = None, comm: Comm | None = None, + self, doc: Document, comm: Comm | None = None, preprocess: bool = True ) -> tuple[Viewable, Model]: if self._updates: @@ -483,7 +483,7 @@ def clone(self: T, object: Optional[Any] = None, **params) -> T: return type(self)(object, **params) def get_root( - self, doc: Optional[Document] = None, comm: Comm | None = None, + self, doc: Document | None = None, comm: Comm | None = None, preprocess: bool = True ) -> Model: """ @@ -529,7 +529,7 @@ class ModelPane(Pane): `bokeh.model.Model` can consume. """ - _bokeh_model: ClassVar[Model] + _bokeh_model: ClassVar[type[Model]] __abstract = True @@ -591,11 +591,11 @@ class ReplacementPane(Pane): _ignored_refs: ClassVar[tuple[str,...]] = ('object',) - _linked_properties: ClassVar[tuple[str,...]] = () + _linked_properties: tuple[str,...] = () _rename: ClassVar[Mapping[str, str | None]] = {'_pane': None, 'inplace': None} - _updates: bool = True + _updates: ClassVar[bool] = True __abstract = True @@ -651,7 +651,7 @@ def _recursive_update(cls, old: Reactive, new: Reactive): The new Reactive component that the old one is being updated or replaced with. """ - ignored = ('name',) + ignored: tuple[str, ...] = ('name',) if isinstance(new, ListPanel): if len(old) == len(new): for i, (sub_old, sub_new) in enumerate(zip(old, new)): diff --git a/panel/pane/equation.py b/panel/pane/equation.py index 4af31999ce..9f7e33ab85 100644 --- a/panel/pane/equation.py +++ b/panel/pane/equation.py @@ -83,8 +83,8 @@ def _get_model( self, doc: Document, root: Model | None = None, parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = self._get_model_type(root, comm) - model = self._bokeh_model(**self._get_properties(doc)) + model_type = self._get_model_type(root, comm) + model = model_type(**self._get_properties(doc)) root = root or model self._models[root.ref['id']] = (model, parent) return model diff --git a/panel/pane/image.py b/panel/pane/image.py index e98c0f60db..9e884d6ab4 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -119,6 +119,7 @@ async def replace_content(): import requests r = requests.request(url=obj, method='GET') return r.content + return None class ImageBase(FileBase): @@ -205,6 +206,8 @@ def _img_dims(self, width, height): def _transform_object(self, obj: Any) -> dict[str, Any]: if self.embed or (isfile(obj) or not isinstance(obj, (str, PurePath))): data = self._data(obj) + elif isinstance(obj, PurePath): + raise ValueError(f"Could not find {type(self).__name__}.object {obj}.") else: w, h = self._img_dims(self.width, self.height) return dict(object=self._format_html(obj, w, h)) @@ -443,6 +446,8 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: if self.embed or (isfile(obj) or (isinstance(obj, str) and obj.lstrip().startswith(' tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return tuple( self._property_mapping.get(p, p) for p in self.param if p not in Viewable.param and self._property_mapping.get(p, p) is not None ) - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None) -> dict[str, Any]: return self._process_param_change(self._init_params()) def _process_property_change(self, msg: dict[str, Any]) -> dict[str, Any]: @@ -215,7 +217,10 @@ def _process_param_change(self, msg: dict[str, Any]) -> dict[str, Any]: wrapped = [] for stylesheet in stylesheets: if isinstance(stylesheet, str) and (stylesheet.split('?')[0].endswith('.css') or stylesheet.startswith('http')): - cache = (state._stylesheets if state.curdoc else {}).get(state.curdoc, {}) + if state.curdoc: + cache = state._stylesheets.get(state.curdoc, {}) + else: + cache = {} if stylesheet in cache: stylesheet = cache[stylesheet] else: @@ -257,7 +262,7 @@ def _link_params(self) -> None: self._internal_callbacks.append(watcher) def _link_props( - self, model: Model, properties: Sequence[str] | Sequence[tuple[str, str]], + self, model: Model | DataModel, properties: Sequence[str] | Sequence[tuple[str, str]], doc: Document, root: Model, comm: Optional[Comm] = None ) -> None: from .config import config @@ -270,9 +275,10 @@ def _link_props( _, p = p m = model if '.' in p: - *subpath, p = p.split('.') - for sp in subpath: + *parts, p = p.split('.') + for sp in parts: m = getattr(m, sp) + subpath = '.'.join(parts) else: subpath = None if comm: @@ -378,7 +384,7 @@ def _update_model( elif ref in self._changing: del self._changing[ref] - def _cleanup(self, root: Model | None) -> None: + def _cleanup(self, root: Model | None = None) -> None: super()._cleanup(root) if root is None: return @@ -528,7 +534,7 @@ def _schedule_change(self, doc: Document, comm: Comm | None) -> None: self._change_event(doc) def _comm_change( - self, doc: Document, ref: str, comm: Comm | None, subpath: str, + self, doc: Document, ref: str, comm: Comm | None, subpath: str | None, attr: str, old: Any, new: Any ) -> None: if subpath: @@ -571,7 +577,7 @@ def _server_event(self, doc: Document, event: Event) -> None: self._comm_event(doc, event) def _server_change( - self, doc: Document, ref: str, subpath: str, attr: str, + self, doc: Document, ref: str, subpath: str | None, attr: str, old: Any, new: Any ) -> None: if subpath: @@ -649,7 +655,7 @@ async def _async_refs(self, *_): # Private API #---------------------------------------------------------------- - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None) -> dict[str, Any]: params, _ = self._design.params(self, doc) if self._design else ({}, None) for k, v in self._init_params().items(): if k in ('stylesheets', 'tags') and k in params: @@ -670,10 +676,11 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: stylesheets = [] for stylesheet in properties['stylesheets']: if isinstance(stylesheet, ImportedStyleSheet): - if stylesheet.url in cache: - stylesheet = cache[stylesheet.url] + url = str(stylesheet.url) + if url in cache: + stylesheet = cache[url] else: - cache[stylesheet.url] = stylesheet + cache[url] = stylesheet patch_stylesheet(stylesheet, dist_url) stylesheets.append(stylesheet) properties['stylesheets'] = stylesheets @@ -863,8 +870,12 @@ def jscallback(self, args: dict[str, Any]={}, **callbacks: str) -> Callback: return Callback(self, code=callbacks, args=args) def jslink( - self, target: JSLinkTarget , code: dict[str, str] = None, args: Optional[dict] = None, - bidirectional: bool = False, **links: str + self, + target: JSLinkTarget, + code: dict[str, str] | None = None, + args: dict | None = None, + bidirectional: bool = False, + **links: str ) -> Link: """ Links properties on the this Reactive object to those on the @@ -915,7 +926,7 @@ def jslink( return Link(self, target, properties=links, code=code, args=args, bidirectional=bidirectional) - def _send_event(self, Event: ModelEvent, **event_kwargs): + def _send_event(self, Event: ModelEvent, **event_kwargs: Any): """ Send an event to the frontend @@ -944,9 +955,6 @@ def _send_event(self, Event: ModelEvent, **event_kwargs): doc.add_next_tick_callback(cb) -TData = Union['pd.DataFrame', 'DataDict'] - - class SyncableData(Reactive): """ A baseclass for components which sync one or more data parameters @@ -982,7 +990,7 @@ def _validate(self, *events: param.parameterized.Event) -> None: Allows implementing validation for the data parameters. """ - def _get_data(self) -> tuple[TData, 'DataDict']: + def _get_data(self) -> tuple[TData, DataDict]: """ Implemented by subclasses converting data parameter(s) into a ColumnDataSource compatible data dictionary. @@ -995,6 +1003,7 @@ def _get_data(self) -> tuple[TData, 'DataDict']: Dictionary of columns used to instantiate and update the ColumnDataSource """ + raise NotImplementedError() def _update_column(self, column: str, array: np.ndarray | list) -> None: """ @@ -1036,7 +1045,7 @@ def _update_cds(self, *events: param.parameterized.Event) -> None: @updating def _update_selected( - self, *events: param.parameterized.Event, indices: Optional[list[int]] = None + self, *events: param.parameterized.Event, indices: list[int] | None = None ) -> None: indices = self.selection if indices is None else indices msg = {'indices': indices} @@ -1044,7 +1053,7 @@ def _update_selected( for ref, (m, _) in self._models.copy().items(): self._apply_update(named_events, msg, m.source.selected, ref) - def _apply_stream(self, ref: str, model: Model, stream: 'DataDict', rollover: Optional[int]) -> None: + def _apply_stream(self, ref: str, model: Model, stream: DataDict, rollover: int | None) -> None: self._changing[ref] = ['data'] try: model.source.stream(stream, rollover) @@ -1052,7 +1061,7 @@ def _apply_stream(self, ref: str, model: Model, stream: 'DataDict', rollover: Op del self._changing[ref] @updating - def _stream(self, stream: 'DataDict', rollover: Optional[int] = None) -> None: + def _stream(self, stream: DataDict, rollover: int | None = None) -> None: self._processed, _ = self._get_data() for ref, (m, _) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: @@ -1067,7 +1076,7 @@ def _stream(self, stream: 'DataDict', rollover: Optional[int] = None) -> None: cb = partial(self._apply_stream, ref, m, stream, rollover) doc.add_next_tick_callback(cb) - def _apply_patch(self, ref: str, model: Model, patch: 'Patches') -> None: + def _apply_patch(self, ref: str, model: Model, patch: Patches) -> None: self._changing[ref] = ['data'] try: model.source.patch(patch) @@ -1075,7 +1084,7 @@ def _apply_patch(self, ref: str, model: Model, patch: 'Patches') -> None: del self._changing[ref] @updating - def _patch(self, patch: 'Patches') -> None: + def _patch(self, patch: Patches) -> None: for ref, (m, _) in self._models.copy().items(): if ref not in state._views or ref in state._fake_roots: continue @@ -1101,8 +1110,8 @@ def _update_manual(self, *events: param.parameterized.Event) -> None: super()._update_manual(*processed_events) def stream( - self, stream_value: 'pd.DataFrame' | 'pd.Series' | dict, - rollover: Optional[int] = None, reset_index: bool = True + self, stream_value: pd.DataFrame | pd.Series | DataDict, + rollover: int | None = None, reset_index: bool = True ) -> None: """ Streams (appends) the `stream_value` provided to the existing @@ -1165,7 +1174,7 @@ def stream( pd = None # type: ignore if pd and isinstance(stream_value, pd.DataFrame): if isinstance(self._processed, dict): - self.stream(stream_value.to_dict(), rollover) + self.stream(stream_value.to_dict(), rollover) # type: ignore return if reset_index: value_index_start = self._processed.index.max() + 1 @@ -1196,10 +1205,10 @@ def stream( if not all(col in stream_value for col in self._data): raise ValueError("Stream update must append to all columns.") for col, array in stream_value.items(): - combined = np.concatenate([self._data[col], array]) + concatenated = np.concatenate([self._data[col], array]) if rollover is not None: - combined = combined[-rollover:] - self._update_column(col, combined) + concatenated = concatenated[-rollover:] + self._update_column(col, concatenated) self._stream(stream_value, rollover) else: try: @@ -1277,12 +1286,16 @@ def patch(self, patch_value: 'pd.DataFrame' | 'pd.Series' | dict) -> None: elif pd and isinstance(patch_value, pd.Series): if "index" in patch_value: # Series orient is row patch_value_dict = { - k: [(patch_value["index"], v)] for k, v in patch_value.items() + str(k): [(int(patch_value["index"]), v)] # type: ignore + for k, v in patch_value.items() } patch_value_dict.pop("index") else: # Series orient is column patch_value_dict = { - patch_value.name: [(index, value) for index, value in patch_value.items()] + str(patch_value.name): [ + (int(index), value) # type: ignore + for index, value in patch_value.items() + ] } self.patch(patch_value_dict) elif isinstance(patch_value, dict): @@ -1329,7 +1342,7 @@ def _convert_column( # timezone-aware, to UTC nanoseconds, to datetime64. converted = ( pd.Series(pd.to_datetime(values, unit="ms")) - .dt.tz_localize(dtype.tz) + .dt.tz_localize(dtype.tz).values ) else: # Timestamps converted from milliseconds to nanoseconds, @@ -1348,11 +1361,14 @@ def _convert_column( converted = new_values elif 'pandas' in sys.modules: import pandas as pd + tmp_values: np.ndarray | list[Any] if Version(pd.__version__) >= Version('1.1.0'): from pandas.core.arrays.masked import BaseMaskedDtype if isinstance(dtype, BaseMaskedDtype): - values = [dtype.na_value if v == '' else v for v in values] - converted = pd.Series(values).astype(dtype).values + tmp_values = [dtype.na_value if v == '' else v for v in values] + else: + tmp_values = values + converted = pd.Series(tmp_values).astype(dtype).values else: converted = values.astype(dtype) return values if converted is None else converted @@ -2036,7 +2052,7 @@ def _get_template(self) -> tuple[str, list[str], Mapping[str, list[tuple[str, li return html, parser.nodes, p_attrs @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: linked_properties = [p for pss in self._attrs.values() for _, ps, _ in pss for p in ps] for scripts in self._scripts.values(): if not isinstance(scripts, list): diff --git a/panel/template/base.py b/panel/template/base.py index a47161dc5e..c715d9f64d 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -103,7 +103,7 @@ class BaseTemplate(param.Parameterized, MimeRenderMixin, ServableMixin, Resource _js: ClassVar[Path | str | list[Path | str] | None] = None # External resources - _resources: ClassVar[dict[str, dict[str, str]]] = { + _resources = { 'css': {}, 'js': {}, 'js_modules': {}, 'tarball': {} } diff --git a/panel/template/react/__init__.py b/panel/template/react/__init__.py index b0378150d9..7f8e0d8058 100644 --- a/panel/template/react/__init__.py +++ b/panel/template/react/__init__.py @@ -1,6 +1,8 @@ """ React template """ +from __future__ import annotations + import json import math import pathlib @@ -59,7 +61,7 @@ def __init__(self, **params): super().__init__(**params) self._update_render_vars() - def _update_render_items(self, event): + def _update_render_items(self, event: param.parameterized.Event): super()._update_render_items(event) if event.obj is not self.main: return diff --git a/panel/tests/pane/test_vega.py b/panel/tests/pane/test_vega.py index 152f910934..0f6e098702 100644 --- a/panel/tests/pane/test_vega.py +++ b/panel/tests/pane/test_vega.py @@ -8,7 +8,7 @@ import altair as alt altair_version = Version(alt.__version__) except Exception: - alt = None + alt = None # type: ignore altair_available = pytest.mark.skipif(alt is None, reason="requires altair") diff --git a/panel/tests/pane/test_vtk.py b/panel/tests/pane/test_vtk.py index 13c5b6a8b2..2535cb7f34 100644 --- a/panel/tests/pane/test_vtk.py +++ b/panel/tests/pane/test_vtk.py @@ -11,12 +11,12 @@ try: import vtk except Exception: - vtk = None + vtk = None # type: ignore try: import pyvista as pv except Exception: - pv = None + pv = None # type: ignore from bokeh.models import ColorBar diff --git a/panel/tests/theme/test_base.py b/panel/tests/theme/test_base.py index 1564ef9c14..cd96a2a392 100644 --- a/panel/tests/theme/test_base.py +++ b/panel/tests/theme/test_base.py @@ -14,7 +14,7 @@ def _custom_repr(self): except Exception: return "ImportedStyleSheet(...)" -ImportedStyleSheet.__repr__ = _custom_repr +ImportedStyleSheet.__repr__ = _custom_repr # type: ignore class DesignTest(Design): diff --git a/panel/tests/ui/pane/test_ipywidget.py b/panel/tests/ui/pane/test_ipywidget.py index f459c1a61e..3ae4d83329 100644 --- a/panel/tests/ui/pane/test_ipywidget.py +++ b/panel/tests/ui/pane/test_ipywidget.py @@ -16,13 +16,13 @@ try: import reacton except Exception: - reacton = None + reacton = None # type: ignore requires_reacton = pytest.mark.skipif(reacton is None, reason="requires reaction") try: import anywidget except Exception: - anywidget = None + anywidget = None # type: ignore requires_anywidget = pytest.mark.skipif(anywidget is None, reason="requires anywidget") pytestmark = pytest.mark.ui diff --git a/panel/tests/ui/pane/test_textual.py b/panel/tests/ui/pane/test_textual.py index 23dc350958..978887b769 100644 --- a/panel/tests/ui/pane/test_textual.py +++ b/panel/tests/ui/pane/test_textual.py @@ -10,7 +10,7 @@ from textual.app import App from textual.widgets import Button except Exception: - textual = None + textual = None # type: ignore textual_available = pytest.mark.skipif(textual is None, reason="requires textual") from panel.pane import Textual diff --git a/panel/tests/ui/pane/test_vega.py b/panel/tests/ui/pane/test_vega.py index 70184c6971..584b0b8281 100644 --- a/panel/tests/ui/pane/test_vega.py +++ b/panel/tests/ui/pane/test_vega.py @@ -5,7 +5,7 @@ try: import altair as alt except Exception: - alt = None + alt = None # type: ignore altair_available = pytest.mark.skipif(alt is None, reason='Requires altair') diff --git a/panel/tests/util.py b/panel/tests/util.py index b3bf34e4af..212b84a4f5 100644 --- a/panel/tests/util.py +++ b/panel/tests/util.py @@ -35,7 +35,7 @@ import holoviews as hv hv_version: Version | None = Version(hv.__version__) except Exception: - hv, hv_version = None, None + hv, hv_version = None, None # type: ignore hv_available = pytest.mark.skipif(hv_version is None or hv_version < Version('1.13.0a23'), reason="requires holoviews") @@ -43,19 +43,19 @@ import matplotlib as mpl mpl.use('Agg') except Exception: - mpl = None + mpl = None # type: ignore mpl_available = pytest.mark.skipif(mpl is None, reason="requires matplotlib") try: import streamz except Exception: - streamz = None + streamz = None # type: ignore streamz_available = pytest.mark.skipif(streamz is None, reason="requires streamz") try: import jupyter_bokeh except Exception: - jupyter_bokeh = None + jupyter_bokeh = None # type: ignore jb_available = pytest.mark.skipif(jupyter_bokeh is None, reason="requires jupyter_bokeh") APP_PATTERN = re.compile(r'Bokeh app running at: http://localhost:(\d+)/') diff --git a/panel/util/checks.py b/panel/util/checks.py index 139c2598ff..9c21d48618 100644 --- a/panel/util/checks.py +++ b/panel/util/checks.py @@ -27,7 +27,7 @@ datetime_types = (np.datetime64, dt.datetime, dt.date) -def isfile(path: str) -> bool: +def isfile(path: str | os.PathLike) -> bool: """Safe version of os.path.isfile robust to path length issues on Windows""" try: return os.path.isfile(path) diff --git a/panel/viewable.py b/panel/viewable.py index 689f900034..5509078cee 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -34,7 +34,9 @@ from param.parameterized import instance_descriptor from pyviz_comms import Comm # type: ignore -from ._param import Align, Aspect, Margin +from ._param import ( + Align, AlignmentEnum, Aspect, Margin, +) from .config import config, panel_extension from .io import serve from .io.document import create_doc_if_none_exists, init_doc @@ -56,6 +58,7 @@ from bokeh.server.server import Server from .io.location import Location + from .io.notebook import Mimebundle from .io.server import StoppableThread from .theme import Design @@ -69,7 +72,7 @@ class Layoutable(param.Parameterized): for all Panel components with a visual representation. """ - align = Align(default='start', doc=""" + align = Align(default=AlignmentEnum.START.value, doc=""" Whether the object should be aligned with the start, end or center of its container. If set as a tuple it will declare (vertical, horizontal) alignment.""") @@ -308,7 +311,11 @@ class ServableMixin: """ def _modify_doc( - self, server_id: str, title: str, doc: Document, location: Location | bool | None + self, + server_id: str | None, + title: str, + doc: Document, + location: Location | bool | None ) -> Document: """ Callback to handle FunctionHandler document creation. @@ -318,10 +325,13 @@ def _modify_doc( return self.server_doc(doc, title, location) # type: ignore def _add_location( - self, doc: Document, location: Optional['Location' | bool], - root: Optional['Model'] = None - ) -> 'Location': + self, + doc: Document, + location: Location | bool, + root: Model | None = None + ) -> Location | None: from .io.location import Location + loc: Location | None if isinstance(location, Location): loc = location state._locations[doc] = loc @@ -330,6 +340,9 @@ def _add_location( else: with set_curdoc(doc): loc = state.location + if loc is None: + return None + if root is None: loc_model = loc.get_root(doc) else: @@ -344,8 +357,8 @@ def _add_location( #---------------------------------------------------------------- def servable( - self, title: Optional[str] = None, location: bool | 'Location' = True, - area: str = 'main', target: Optional[str] = None + self, title: str | None = None, location: bool | Location = True, + area: str = 'main', target: str | None = None ) -> 'ServableMixin': """ Serves the object or adds it to the configured @@ -373,7 +386,8 @@ def servable( ------- The Panel object itself """ - if curdoc_locked().session_context: + doc = curdoc_locked() + if doc and doc.session_context: logger = logging.getLogger('bokeh') for handler in logger.handlers: if isinstance(handler, logging.StreamHandler): @@ -414,10 +428,10 @@ def servable( return self def show( - self, title: Optional[str] = None, port: int = 0, address: Optional[str] = None, - websocket_origin: Optional[str] = None, threaded: bool = False, verbose: bool = True, - open: bool = True, location: bool | 'Location' = True, **kwargs - ) -> 'StoppableThread' | 'Server': + self, title: str | None = None, port: int = 0, address: str | None = None, + websocket_origin: str | None = None, threaded: bool = False, verbose: bool = True, + open: bool = True, location: bool | Location = True, **kwargs + ) -> StoppableThread | Server: """ Starts a Bokeh server and displays the Viewable in a new tab. @@ -548,9 +562,9 @@ def _log(self, msg: str, *args, level: str = 'debug') -> None: getattr(self._logger, level)(f'Session %s {msg}', id(state.curdoc), *args) def _get_model( - self, doc: Document, root: Optional['Model'] = None, - parent: Optional['Model'] = None, comm: Optional[Comm] = None - ) -> 'Model': + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None + ) -> Model: """ Converts the objects being wrapped by the viewable into a bokeh model that can be composed in a bokeh layout. @@ -605,7 +619,7 @@ def _preprocess(self, root: 'Model', changed=None, old_models=None) -> None: except TypeError: hook(self, root) - def _render_model(self, doc: Optional[Document] = None, comm: Optional[Comm] = None) -> 'Model': + def _render_model(self, doc: Document | None = None, comm: Comm | None = None) -> Model: if doc is None: doc = Document() if comm is None: @@ -626,7 +640,7 @@ def _render_model(self, doc: Optional[Document] = None, comm: Optional[Comm] = N def _init_params(self) -> Mapping[str, Any]: return {k: v for k, v in self.param.values().items() if v is not None} - def _server_destroy(self, session_context: 'BokehSessionContext') -> None: + def _server_destroy(self, session_context: BokehSessionContext) -> None: """ Server lifecycle hook triggered when session is destroyed. """ @@ -645,7 +659,7 @@ def __repr__(self, depth: int = 0) -> str: params=', '.join(param_reprs(self))) def get_root( - self, doc: Optional[Document] = None, comm: Optional[Comm] = None, + self, doc: Document | None = None, comm: Comm | None = None, preprocess: bool = True ) -> Model: """ @@ -683,6 +697,7 @@ def get_root( state._views[ref] = (root_view, root, doc, comm) return root + class Viewable(Renderable, Layoutable, ServableMixin): """ Viewable is the baseclass all visual components in the panel @@ -732,7 +747,7 @@ def _update_loading(self, *_) -> None: else: stop_loading_spinner(self) - def _render_model(self, doc: Optional[Document] = None, comm: Optional[Comm] = None) -> 'Model': + def _render_model(self, doc: Document | None = None, comm: Comm | None = None) -> Model: if doc is None: doc = Document() if comm is None: @@ -857,8 +872,8 @@ def clone(self, **params) -> 'Viewable': return type(self)(**dict(inherited, **params)) def select( - self, selector: Optional[type | Callable[['Viewable'], bool]] = None - ) -> list['Viewable']: + self, selector: type | Callable[[Viewable], bool] | None = None + ) -> list[Viewable]: """ Iterates over the Viewable and any potential children in the applying the Selector. @@ -882,9 +897,9 @@ def select( def embed( self, max_states: int = 1000, max_opts: int = 3, json: bool = False, - json_prefix: str = '', save_path: str = './', load_path: Optional[str] = None, + json_prefix: str = '', save_path: str = './', load_path: str | None = None, progress: bool = False, states={} - ) -> None: + ) -> Mimebundle: """ Renders a static version of a panel in a notebook by evaluating the set of states defined by the widgets in the model. Note @@ -1016,13 +1031,17 @@ def server_doc( if location: self._add_location(doc, location, model) if config.notifications and doc is state.curdoc: - notification_model = state.notifications.get_root(doc) - notification_model.name = 'notifications' - doc.add_root(notification_model) + notification = state.notifications + if notification: + notification_model = notification.get_root(doc) + notification_model.name = 'notifications' + doc.add_root(notification_model) if config.browser_info and doc is state.curdoc: - browser_model = state.browser_info._get_model(doc, model) - browser_model.name = 'browser_info' - doc.add_root(browser_model) + browser = state.browser_info + if browser: + browser_model = browser._get_model(doc, model) + browser_model.name = 'browser_info' + doc.add_root(browser_model) return doc @@ -1083,7 +1102,7 @@ class Child(param.ClassSelector): by calling the `pn.panel` utility. """ - @typing.overload + @typing.overload # type: ignore def __init__( self, default=None, *, is_instance=True, allow_None=False, doc=None, diff --git a/panel/widgets/base.py b/panel/widgets/base.py index 74b13adf0d..40c14dbdb1 100644 --- a/panel/widgets/base.py +++ b/panel/widgets/base.py @@ -96,10 +96,10 @@ class Widget(Reactive, WidgetBase): _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title'} # Whether the widget supports embedding - _supports_embed: ClassVar[bool] = False + _supports_embed: bool = False # Declares the Bokeh model type of the widget - _widget_type: ClassVar[type[Model] | None] = None + _widget_type: ClassVar[type[Model]] __abstract = True @@ -115,7 +115,7 @@ def __init__(self, **params: Any): super().__init__(**params) @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: props = list(super()._linked_properties) if 'description' in props: props.remove('description') @@ -196,7 +196,7 @@ class CompositeWidget(Widget): _composite_type: ClassVar[type[ListPanel]] = Row - _linked_properties: ClassVar[tuple[str]] = () + _linked_properties: tuple[str, ...] = () __abstract = True diff --git a/panel/widgets/indicators.py b/panel/widgets/indicators.py index 65c434565a..372cd9a722 100644 --- a/panel/widgets/indicators.py +++ b/panel/widgets/indicators.py @@ -70,7 +70,7 @@ class Indicator(Widget): 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) - _linked_properties: ClassVar[tuple[str,...]] = () + _linked_properties: tuple[str,...] = () _rename: ClassVar[Mapping[str, str | None]] = {'name': None} diff --git a/panel/widgets/input.py b/panel/widgets/input.py index b5337e977f..a7dd347568 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -255,7 +255,7 @@ def _process_param_change(self, msg): return msg @property - def _linked_properties(self): + def _linked_properties(self) -> tuple[str, ...]: properties = super()._linked_properties return properties + ('filename',) @@ -449,7 +449,7 @@ class StaticText(Widget): _widget_type: ClassVar[type[Model]] = _BkDiv @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return () def _init_params(self) -> dict[str, Any]: @@ -1004,7 +1004,7 @@ def __repr__(self, depth=0): params=', '.join(param_reprs(self, ['value_throttled']))) @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return super()._linked_properties + ('value_throttled',) def _update_model( diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 305ad5f0fa..a2cc5b7837 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -84,7 +84,7 @@ def __repr__(self, depth=0): params=', '.join(param_reprs(self, ['value_throttled']))) @property - def _linked_properties(self) -> tuple[str]: + def _linked_properties(self) -> tuple[str, ...]: return super()._linked_properties + ('value_throttled',) def _process_property_change(self, msg): diff --git a/pyproject.toml b/pyproject.toml index a4e2217daa..cb5e29abb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -268,6 +268,7 @@ module = [ "pyecharts.*", "pyodide_http.*", "pyodide.*", + "pyscript.*", "pyvista.*", "pyviz_comms.*", "rpy2.*",