From 7afcc9023e6193aab427848890d13412e61ce075 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 2 Dec 2024 20:03:20 +0100 Subject: [PATCH] Enable strict type checking (#7497) --- .github/workflows/test.yaml | 2 +- doc/generate_modules.py | 36 +-- examples/apps/django/sliders/sinewave.py | 2 +- .../apps/django_multi_apps/gbm/pn_model.py | 2 +- .../django_multi_apps/sliders/pn_model.py | 2 +- .../stockscreener/pn_model.py | 2 +- .../stockscreener/pn_model_20122019.py | 2 +- examples/apps/fastApi/sliders/sinewave.py | 2 +- .../fastApi_multi_apps/sliders/sinewave.py | 2 +- .../fastApi_multi_apps/sliders2/sinewave.py | 2 +- panel/__main__.py | 2 +- panel/_param.py | 46 +++- panel/auth.py | 7 +- panel/chat/feed.py | 29 +- panel/chat/input.py | 13 +- panel/chat/interface.py | 38 ++- panel/chat/langchain.py | 8 +- panel/chat/message.py | 27 +- panel/chat/step.py | 3 +- panel/chat/utils.py | 2 +- panel/command/__init__.py | 91 +++---- panel/command/bundle.py | 18 +- panel/command/compile.py | 6 +- panel/command/convert.py | 40 +-- panel/command/serve.py | 78 +++--- panel/config.py | 20 +- panel/custom.py | 82 +++--- panel/entry_points.py | 4 +- panel/io/admin.py | 9 +- panel/io/application.py | 35 ++- panel/io/browser.py | 5 +- panel/io/cache.py | 29 +- panel/io/compile.py | 52 ++-- panel/io/convert.py | 60 ++-- panel/io/datamodel.py | 5 +- panel/io/django.py | 2 +- panel/io/document.py | 63 +++-- panel/io/fastapi.py | 22 +- panel/io/handlers.py | 47 +++- panel/io/ipywidget.py | 10 +- panel/io/jupyter_executor.py | 18 +- panel/io/jupyter_server_extension.py | 25 +- panel/io/location.py | 23 +- panel/io/mime_render.py | 8 +- panel/io/model.py | 19 +- panel/io/notebook.py | 31 ++- panel/io/notifications.py | 8 +- panel/io/profile.py | 19 +- panel/io/pyodide.py | 38 ++- panel/io/reload.py | 21 +- panel/io/resources.py | 61 +++-- panel/io/save.py | 35 ++- panel/io/server.py | 126 +++++---- panel/io/state.py | 199 ++++++++------ panel/io/threads.py | 2 +- panel/layout/accordion.py | 5 +- panel/layout/base.py | 36 +-- panel/layout/card.py | 5 +- panel/layout/feed.py | 11 +- panel/layout/grid.py | 18 +- panel/layout/gridstack.py | 5 +- panel/layout/tabs.py | 3 +- panel/links.py | 82 +++--- panel/models/ace.py | 4 +- panel/models/datetime_slider.py | 4 +- panel/models/deckgl.py | 4 +- panel/models/icon.py | 2 +- panel/models/trend.py | 10 +- panel/models/vega.py | 14 +- panel/models/vtk.py | 4 +- panel/models/widgets.py | 14 +- panel/pane/alert.py | 3 +- panel/pane/base.py | 51 ++-- panel/pane/deckgl.py | 18 +- panel/pane/echarts.py | 13 +- panel/pane/equation.py | 11 +- panel/pane/holoviews.py | 13 +- panel/pane/image.py | 12 +- panel/pane/ipywidget.py | 14 +- panel/pane/markup.py | 17 +- panel/pane/media.py | 15 +- panel/pane/perspective.py | 13 +- panel/pane/plot.py | 13 +- panel/pane/plotly.py | 18 +- panel/pane/streamz.py | 9 +- panel/pane/vega.py | 13 +- panel/pane/vizzu.py | 11 +- panel/pane/vtk/enums.py | 6 +- panel/pane/vtk/synchronizable_serializer.py | 4 +- panel/pane/vtk/vtk.py | 35 +-- panel/param.py | 45 ++- panel/reactive.py | 166 ++++++----- panel/template/base.py | 67 +++-- panel/template/editable/__init__.py | 8 +- panel/template/golden/__init__.py | 2 +- panel/template/react/__init__.py | 6 +- panel/template/slides/__init__.py | 2 +- panel/template/vanilla/__init__.py | 6 +- panel/tests/chat/test_langchain.py | 1 - panel/tests/conftest.py | 2 +- panel/tests/io/test_cache.py | 2 +- panel/tests/io/test_document.py | 2 - panel/tests/io/test_handlers.py | 2 +- panel/tests/manual/models.py | 257 ------------------ panel/tests/pane/test_vega.py | 2 +- panel/tests/pane/test_vtk.py | 4 +- panel/tests/test_models.py | 2 +- panel/tests/test_reactive.py | 3 +- panel/tests/test_server.py | 2 +- panel/tests/test_viewable.py | 14 +- panel/tests/theme/test_base.py | 2 +- panel/tests/ui/jupyter_server_test_config.py | 1 + panel/tests/ui/layout/test_gridspec.py | 1 - panel/tests/ui/pane/test_ipywidget.py | 6 +- panel/tests/ui/pane/test_textual.py | 2 +- panel/tests/ui/pane/test_vega.py | 2 +- panel/tests/util.py | 16 +- panel/theme/base.py | 31 ++- panel/util/__init__.py | 4 +- panel/util/checks.py | 5 +- panel/util/parameters.py | 3 +- panel/util/warnings.py | 2 +- panel/viewable.py | 110 ++++---- panel/widgets/_mixin.py | 3 +- panel/widgets/base.py | 28 +- panel/widgets/button.py | 21 +- panel/widgets/codeeditor.py | 18 +- panel/widgets/debugger.py | 7 +- panel/widgets/file_selector.py | 18 +- panel/widgets/icon.py | 3 +- panel/widgets/indicators.py | 17 +- panel/widgets/input.py | 29 +- panel/widgets/misc.py | 10 +- panel/widgets/player.py | 5 +- panel/widgets/select.py | 21 +- panel/widgets/slider.py | 13 +- panel/widgets/speech_to_text.py | 3 +- panel/widgets/tables.py | 119 +++++--- panel/widgets/terminal.py | 10 +- panel/widgets/text_to_speech.py | 5 +- panel/widgets/texteditor.py | 18 +- panel/widgets/widget.py | 43 +-- pixi.toml | 3 +- pyproject.toml | 18 +- scripts/panelite/generate_panelite_content.py | 2 +- 145 files changed, 1635 insertions(+), 1609 deletions(-) delete mode 100644 panel/tests/manual/models.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f1ce5125cb..44e740c106 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -270,7 +270,7 @@ jobs: environments: ${{ matrix.environment }} - name: Test Type run: | - pixi run -e ${{ matrix.environment }} test-type || echo "Failed" + pixi run -e ${{ matrix.environment }} test-type result_test_suite: name: result:test diff --git a/doc/generate_modules.py b/doc/generate_modules.py index 0ec9bfb8ae..17666fad36 100644 --- a/doc/generate_modules.py +++ b/doc/generate_modules.py @@ -57,11 +57,11 @@ def write_file(name, text, opts): """Write the output file for module/package .""" if opts.dryrun: return - fname = os.path.join(opts.destdir, "%s.%s" % (name, opts.suffix)) + fname = os.path.join(opts.destdir, f"{name}.{opts.suffix}") if not opts.force and os.path.isfile(fname): - print('File %s already exists, skipping.' % fname) + print(f'File {fname} already exists, skipping.') else: - print('Creating file %s.' % fname) + print(f'Creating file {fname}.') f = open(fname, 'w') f.write(text) f.close() @@ -69,27 +69,28 @@ def write_file(name, text, opts): def format_heading(level, text): """Create a heading of [1, 2 or 3 supported].""" underlining = ['=', '-', '~', ][level-1] * len(text) - return '%s\n%s\n\n' % (text, underlining) + return f'{text}\n{underlining}\n\n' def format_directive(module, package=None): """Create the automodule directive and add the options.""" - directive = '.. automodule:: %s\n' % makename(package, module) + module_name = makename(package, module) + directive = f'.. automodule:: %s{module_name}' for option in OPTIONS: - directive += ' :%s:\n' % option + directive += f' :{option}:\n' return directive def create_module_file(package, module, opts): """Build the text of the file and write the file.""" - text = format_heading(1, '%s Module' % module) - text += format_heading(2, ':mod:`%s` Module' % module) + text = format_heading(1, f'{module} Module') + text += format_heading(2, f':mod:`{module}` Module') text += format_directive(module, package) write_file(makename(package, module), text, opts) def create_package_file(root, master_package, subroot, py_files, opts, subs): """Build the text of the file and write the file.""" package = os.path.split(root)[-1] - text = format_heading(1, '%s.%s Package' % (master_package, package)) + text = format_heading(1, f'{master_package}.{package} Package') text += '\n---------\n\n' # add each package's module for py_file in py_files: @@ -99,9 +100,9 @@ def create_package_file(root, master_package, subroot, py_files, opts, subs): py_file = os.path.splitext(py_file)[0] py_path = makename(subroot, py_file) if is_package: - heading = ':mod:`%s` Package' % package + heading = f':mod:`{package}` Package' else: - heading = ':mod:`%s` Module' % py_file + heading = f':mod:`{py_file}` Module' text += format_heading(2, heading) text += '\n\n' text += format_directive(is_package and subroot or py_path, master_package) @@ -113,8 +114,9 @@ def create_package_file(root, master_package, subroot, py_files, opts, subs): if subs: text += format_heading(2, 'Subpackages') text += '.. toctree::\n\n' + subpackage_name = makename(master_package, subroot) for sub in subs: - text += ' %s.%s\n' % (makename(master_package, subroot), sub) + text += f' {subpackage_name}.{sub}\n' text += '\n' write_file(makename(master_package, subroot), text, opts) @@ -123,9 +125,9 @@ def create_modules_toc_file(master_package, modules, opts, name='modules'): """ Create the module's index. """ - text = format_heading(1, '%s Modules' % opts.header) + text = format_heading(1, f'{opts.header} Modules') text += '.. toctree::\n' - text += ' :maxdepth: %s\n\n' % opts.maxdepth + text += f' :maxdepth: {opts.maxdepth}\n\n' modules.sort() prev_module = '' @@ -134,7 +136,7 @@ def create_modules_toc_file(master_package, modules, opts, name='modules'): if module.startswith(prev_module + '.'): continue prev_module = module - text += ' %s\n' % module + text += f' {module}\n' write_file(name, text, opts) @@ -261,9 +263,9 @@ def main(): excludes = normalize_excludes(rootpath, excludes) recurse_tree(rootpath, excludes, opts) else: - print('%s is not a valid output destination directory.' % opts.destdir) + print(f'{opts.destdir} is not a valid output destination directory.') else: - print('%s is not a valid directory.' % rootpath) + print(f'{rootpath} is not a valid directory.') if __name__ == '__main__': diff --git a/examples/apps/django/sliders/sinewave.py b/examples/apps/django/sliders/sinewave.py index a95c0d2af1..c9bb88f0f3 100644 --- a/examples/apps/django/sliders/sinewave.py +++ b/examples/apps/django/sliders/sinewave.py @@ -16,7 +16,7 @@ class SineWave(param.Parameterized): y_range = param.Range(default=(-2.5,2.5),bounds=(-10,10)) def __init__(self, **params): - super(SineWave, self).__init__(**params) + super().__init__(**params) x, y = self.sine() self.cds = ColumnDataSource(data=dict(x=x, y=y)) self.plot = figure(height=400, width=400, diff --git a/examples/apps/django_multi_apps/gbm/pn_model.py b/examples/apps/django_multi_apps/gbm/pn_model.py index eb728b48f5..2f9baf4105 100644 --- a/examples/apps/django_multi_apps/gbm/pn_model.py +++ b/examples/apps/django_multi_apps/gbm/pn_model.py @@ -17,7 +17,7 @@ class GBM(param.Parameterized): refresh = pn.widgets.Button(name="Refresh", button_type='primary') def __init__(self, **params): - super(GBM, self).__init__(**params) + super().__init__(**params) # update the plot for every changes # @param.depends('mean', 'volatility', 'maturity', 'n_observations', 'n_simulations', watch=True) diff --git a/examples/apps/django_multi_apps/sliders/pn_model.py b/examples/apps/django_multi_apps/sliders/pn_model.py index 56a2e32295..4d17bdd658 100644 --- a/examples/apps/django_multi_apps/sliders/pn_model.py +++ b/examples/apps/django_multi_apps/sliders/pn_model.py @@ -15,7 +15,7 @@ class SineWave(param.Parameterized): y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10)) def __init__(self, **params): - super(SineWave, self).__init__(**params) + super().__init__(**params) x, y = self.sine() self.cds = ColumnDataSource(data=dict(x=x, y=y)) self.plot = figure(height=400, width=400, diff --git a/examples/apps/django_multi_apps/stockscreener/pn_model.py b/examples/apps/django_multi_apps/stockscreener/pn_model.py index 787834cdbe..8916c885ba 100644 --- a/examples/apps/django_multi_apps/stockscreener/pn_model.py +++ b/examples/apps/django_multi_apps/stockscreener/pn_model.py @@ -19,7 +19,7 @@ class StockScreener(param.Parameterized): def __init__(self, df, **params): start = dt.date(year=df.index[0].year, month=df.index[0].month, day=df.index[0].day) end = dt.date(year=df.index[-1].year, month=df.index[-1].month, day=df.index[-1].day) - super(StockScreener, self).__init__(df=df, start=start, **params) + super().__init__(df=df, start=start, **params) self.param.start.bounds = (start, end) columns = list(self.df.columns) self.param.index.objects = columns diff --git a/examples/apps/django_multi_apps/stockscreener/pn_model_20122019.py b/examples/apps/django_multi_apps/stockscreener/pn_model_20122019.py index 76eb73335b..7364592bb7 100644 --- a/examples/apps/django_multi_apps/stockscreener/pn_model_20122019.py +++ b/examples/apps/django_multi_apps/stockscreener/pn_model_20122019.py @@ -41,7 +41,7 @@ class StockScreener(param.Parameterized): From = pn.widgets.DateSlider() def __init__(self, df, **params): - super(StockScreener, self).__init__(**params) + super().__init__(**params) # init df self.df = df self.start_date = dt.datetime(year=df.index[0].year, month=df.index[0].month, day=df.index[0].day) diff --git a/examples/apps/fastApi/sliders/sinewave.py b/examples/apps/fastApi/sliders/sinewave.py index 56a2e32295..4d17bdd658 100644 --- a/examples/apps/fastApi/sliders/sinewave.py +++ b/examples/apps/fastApi/sliders/sinewave.py @@ -15,7 +15,7 @@ class SineWave(param.Parameterized): y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10)) def __init__(self, **params): - super(SineWave, self).__init__(**params) + super().__init__(**params) x, y = self.sine() self.cds = ColumnDataSource(data=dict(x=x, y=y)) self.plot = figure(height=400, width=400, diff --git a/examples/apps/fastApi_multi_apps/sliders/sinewave.py b/examples/apps/fastApi_multi_apps/sliders/sinewave.py index 56a2e32295..4d17bdd658 100644 --- a/examples/apps/fastApi_multi_apps/sliders/sinewave.py +++ b/examples/apps/fastApi_multi_apps/sliders/sinewave.py @@ -15,7 +15,7 @@ class SineWave(param.Parameterized): y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10)) def __init__(self, **params): - super(SineWave, self).__init__(**params) + super().__init__(**params) x, y = self.sine() self.cds = ColumnDataSource(data=dict(x=x, y=y)) self.plot = figure(height=400, width=400, diff --git a/examples/apps/fastApi_multi_apps/sliders2/sinewave.py b/examples/apps/fastApi_multi_apps/sliders2/sinewave.py index 56a2e32295..4d17bdd658 100644 --- a/examples/apps/fastApi_multi_apps/sliders2/sinewave.py +++ b/examples/apps/fastApi_multi_apps/sliders2/sinewave.py @@ -15,7 +15,7 @@ class SineWave(param.Parameterized): y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10)) def __init__(self, **params): - super(SineWave, self).__init__(**params) + super().__init__(**params) x, y = self.sine() self.cds = ColumnDataSource(data=dict(x=x, y=y)) self.plot = figure(height=400, width=400, diff --git a/panel/__main__.py b/panel/__main__.py index f4e05fdd32..e4fd43c313 100644 --- a/panel/__main__.py +++ b/panel/__main__.py @@ -1,7 +1,7 @@ import sys -def main(): +def main() -> None: from panel.command import main as _main # Main entry point (see setup.py) diff --git a/panel/_param.py b/panel/_param.py index 0c8e0c72f8..d7053a202f 100644 --- a/panel/_param.py +++ b/panel/_param.py @@ -1,5 +1,16 @@ +from __future__ import annotations + +from typing import ( + Any, Literal, TypeAlias, cast, +) + +from bokeh.core.enums import enumeration from param import Parameter, _is_number +AlignmentType = Literal["auto", "start", "center", "end"] +Alignment = enumeration(AlignmentType) +MarginType: TypeAlias = int | tuple[int, int] | tuple[int, int, int, int] + class Align(Parameter): """ @@ -8,16 +19,20 @@ class Align(Parameter): to the (vertical, horizontal) alignment. """ - def __init__(self, default='start', **params): + def __init__( + self, + default: AlignmentType | tuple[AlignmentType, AlignmentType] = "start", + **params: Any + ): super().__init__(default=default, **params) self._validate(default) - def _validate(self, val): + def _validate(self, val: Any) -> None: self._validate_value(val, self.allow_None) - def _validate_value(self, val, allow_None, valid=('auto', 'start', 'center', 'end')): - if ((val is None and allow_None) or val in valid or - (isinstance(val, tuple) and len(val) == 2 and all(v in valid for v in val))): + def _validate_value(self, val: Any, allow_None: bool) -> None: + if ((val is None and allow_None) or val in Alignment or + (isinstance(val, tuple) and len(val) == 2 and all(v in Alignment for v in val))): return raise ValueError( f"Align parameter {self.name!r} must be one of 'start', " @@ -36,10 +51,10 @@ def __init__(self, default=None, allow_None=True, **params): super().__init__(default=default, allow_None=allow_None, **params) self._validate(default) - def _validate(self, val): + def _validate(self, val: Any) -> None: self._validate_value(val, self.allow_None) - def _validate_value(self, val, allow_None): + def _validate_value(self, val: Any, allow_None: bool) -> None: if (val is None and allow_None) or val == 'auto' or _is_number(val): return raise ValueError( @@ -60,7 +75,7 @@ def __init__(self, default=None, allow_None=True, **params): super().__init__(default=default, allow_None=allow_None, **params) self._validate(default) - def _validate_value(self, val, allow_None): + def _validate_value(self, val: Any, allow_None: bool) -> None: if val is None and allow_None: return if not isinstance(val, (tuple, int)): @@ -69,7 +84,7 @@ def _validate_value(self, val, allow_None): f'tuple values, not values of not {type(val)!r}.' ) - def _validate_length(self, val): + def _validate_length(self, val: Any) -> None: if not isinstance(val, tuple) or len(val) in (2, 4): return raise ValueError( @@ -78,18 +93,23 @@ def _validate_length(self, val): '(top, right, bottom, left).' ) - def _validate(self, val): + def _validate(self, val: Any) -> None: self._validate_value(val, self.allow_None) self._validate_length(val) @classmethod - def serialize(cls, value): + def serialize(cls, value: MarginType) -> Literal['null'] | list[int] | int: if value is None: return 'null' return list(value) if isinstance(value, tuple) else value @classmethod - def deserialize(cls, value): + def deserialize(cls, value: Literal['null'] | list[int] | int) -> MarginType | None: if value == 'null': return None - return tuple(value) if isinstance(value, list) else value + elif isinstance(value, list): + n = len(value) + if (n < 2 or n > 5): + raise ValueError(f'Cannot deserialize list of length {n}.') + return cast(MarginType, tuple(value)) + return value diff --git a/panel/auth.py b/panel/auth.py index 28c206c11a..04e009dd87 100644 --- a/panel/auth.py +++ b/panel/auth.py @@ -12,6 +12,7 @@ from base64 import urlsafe_b64encode from functools import partial +from typing import ClassVar import tornado @@ -101,13 +102,13 @@ class OAuthLoginHandler(tornado.web.RequestHandler, OAuth2Mixin): 'grant_type': 'authorization_code' } - _access_token_header = None + _access_token_header: ClassVar[str | None] = None - _state_cookie = None + _state_cookie: ClassVar[str | None] = None _error_template = ERROR_TEMPLATE - _login_endpoint = '/login' + _login_endpoint: ClassVar[str] = '/login' @property def _SCOPE(self): diff --git a/panel/chat/feed.py b/panel/chat/feed.py index da77313ef4..bf3f129edf 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -8,6 +8,7 @@ import asyncio import traceback +from collections.abc import Callable from enum import Enum from inspect import ( getfullargspec, isasyncgen, isasyncgenfunction, isawaitable, @@ -15,7 +16,7 @@ ) from io import BytesIO from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Literal, + TYPE_CHECKING, Any, ClassVar, Literal, ) import param @@ -409,7 +410,7 @@ def _upsert_message( is_stopped = self._callback_future is not None and self._callback_future.cancelled() if value is None: # don't add new message if the callback returns None - return + return None elif is_stopping or is_stopped: raise StopCallback("Callback was stopped.") @@ -491,12 +492,14 @@ async def _serialize_response(self, response: Any) -> ChatMessage | None: self._callback_state = CallbackState.GENERATING async for token in response: response_message = self._upsert_message(token, response_message) - response_message.show_activity_dot = self.show_activity_dot + if response_message is not None: + response_message.show_activity_dot = self.show_activity_dot elif isgenerator(response): self._callback_state = CallbackState.GENERATING for token in response: response_message = self._upsert_message(token, response_message) - response_message.show_activity_dot = self.show_activity_dot + if response_message is not None: + response_message.show_activity_dot = self.show_activity_dot elif isawaitable(response): response_message = self._upsert_message(await response, response_message) else: @@ -527,7 +530,7 @@ async def _schedule_placeholder( return await asyncio.sleep(0.1) - async def _handle_callback(self, message, loop: asyncio.BaseEventLoop): + async def _handle_callback(self, message, loop: asyncio.AbstractEventLoop): callback_args, callback_kwargs = self._gather_callback_args(message) if iscoroutinefunction(self.callback): response = await self.callback(*callback_args, **callback_kwargs) @@ -561,16 +564,16 @@ async def _prepare_response(self, *_) -> None: num_entries = len(self._chat_log) loop = asyncio.get_event_loop() - future = loop.create_task(self._handle_callback(message, loop)) - self._callback_future = future + task = loop.create_task(self._handle_callback(message, loop)) + self._callback_future = task await asyncio.gather( - self._schedule_placeholder(future, num_entries), future, + self._schedule_placeholder(task, num_entries), task, ) except StopCallback: # callback was stopped by user self._callback_state = CallbackState.STOPPED except Exception as e: - send_kwargs = dict(user="Exception", respond=False) + send_kwargs: dict[str, Any] = dict(user="Exception", respond=False) if self.callback_exception == "summary": self.send( f"Encountered `{e!r}`. " @@ -633,7 +636,7 @@ def send( "Cannot set user or avatar when explicitly sending " "a ChatMessage. Set them directly on the ChatMessage." ) - message = value + message: ChatMessage | None = value else: if not isinstance(value, dict): value = {"object": value} @@ -690,7 +693,7 @@ def stream( "a ChatMessage. Set them directly on the ChatMessage." ) elif message: - if isinstance(value, (str, dict)): + if isinstance(value, str): message.stream(value, replace=replace) if user: message.user = user @@ -848,7 +851,7 @@ def prompt_user( timeout_button_params : dict | None Additional parameters to pass to the timeout button. """ - async def _prepare_prompt(*_) -> None: + async def _prepare_prompt(*args) -> None: input_button_params = button_params or {} if "name" not in input_button_params: input_button_params["name"] = "Submit" @@ -958,7 +961,7 @@ def _serialize_for_transformers( self, messages: list[ChatMessage], role_names: dict[str, str | list[str]] | None = None, - default_role: str | None = "assistant", + default_role: str = "assistant", custom_serializer: Callable | None = None, **serialize_kwargs ) -> list[dict[str, Any]]: diff --git a/panel/chat/input.py b/panel/chat/input.py index 602941613c..5f885cc839 100644 --- a/panel/chat/input.py +++ b/panel/chat/input.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -79,7 +78,7 @@ class ChatAreaInput(_PnTextAreaInput): **_PnTextAreaInput._rename, } - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None = None) -> dict[str, Any]: props = super()._get_properties(doc) props.update({"value_input": self.value, "value": self.value}) return props @@ -87,9 +86,9 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: def _get_model( self, doc: Document, - root: Optional[Model] = None, - parent: Optional[Model] = None, - comm: Optional[Comm] = None, + root: Model | None = None, + parent: Model | None = None, + comm: Comm | None = None, ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events("chat_message_event", model=model, doc=doc, comm=comm) diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 49998e62b3..5052491749 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -6,15 +6,17 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from functools import partial from io import BytesIO -from typing import Any, Callable, ClassVar +from typing import Any, ClassVar import param from ..io.resources import CDN_DIST from ..layout import Row, Tabs +from ..layout.base import ListLike, NamedListLike from ..pane.image import ImageBase from ..viewable import Viewable from ..widgets.base import WidgetBase @@ -70,8 +72,8 @@ class ChatInterface(ChatFeed): >>> yield contents >>> chat_interface = ChatInterface( - callback=repeat_contents, widgets=[TextInput(), FileInput()] - ) + ... callback=repeat_contents, widgets=[TextInput(), FileInput()] + ... ) """ auto_send_types = param.List(doc=""" @@ -186,9 +188,10 @@ def _link_disabled_loading(self, obj: Viewable): Link the disabled and loading attributes of the chat box to the given object. """ - for attr in ["disabled", "loading"]: - setattr(obj, attr, getattr(self, attr)) - self.link(obj, **{attr: attr}) + mapping: dict[str, Any] = {"disabled": "disabled", "loading": "loading"} + values = {p: getattr(self, p) for p in mapping} + self.param.update(values) + self.link(obj, **mapping) @param.depends("width", watch=True) def _update_input_width(self): @@ -389,7 +392,7 @@ def wrapper(self, event: param.parameterized.Event): def _click_send( self, event: param.parameterized.Event | None = None, - instance: "ChatInterface" | None = None + instance: ChatInterface | None = None ) -> None: """ Send the input when the user presses Enter. @@ -430,7 +433,7 @@ def _click_send( def _click_stop( self, event: param.parameterized.Event | None = None, - instance: "ChatInterface" | None = None + instance: ChatInterface | None = None ) -> bool: """ Cancel the callback when the user presses the Stop button. @@ -479,7 +482,7 @@ def _reset_button_data(self): def _click_rerun( self, event: param.parameterized.Event | None = None, - instance: "ChatInterface" | None = None + instance: ChatInterface | None = None ) -> None: """ Upon clicking the rerun button, rerun the last user message, @@ -494,7 +497,7 @@ def _click_rerun( def _click_undo( self, event: param.parameterized.Event | None = None, - instance: "ChatInterface" | None = None + instance: ChatInterface | None = None ) -> None: """ Upon clicking the undo button, undo (remove) messages @@ -518,7 +521,7 @@ def _click_undo( def _click_clear( self, event: param.parameterized.Event | None = None, - instance: "ChatInterface" | None = None + instance: ChatInterface | None = None ) -> None: """ Upon clicking the clear button, clear the chat log. @@ -548,7 +551,11 @@ def active_widget(self) -> WidgetBase: The active widget. """ if isinstance(self._input_layout, Tabs): - return self._input_layout[self.active].objects[0] + current_tab = self._input_layout[self.active] + if isinstance(current_tab, (ListLike, NamedListLike)): + return current_tab.objects[0] + else: + return current_tab # type: ignore return self._input_layout.objects[0] @property @@ -583,8 +590,8 @@ def _serialize_for_transformers( self, messages: list[ChatMessage], role_names: dict[str, str | list[str]] | None = None, - default_role: str | None = "assistant", - custom_serializer: Callable = None, + default_role: str = "assistant", + custom_serializer: Callable[[ChatMessage], Any] | None = None, **serialize_kwargs ) -> list[dict[str, Any]]: """ @@ -620,7 +627,8 @@ def _serialize_for_transformers( "assistant": [self.callback_user], } return super()._serialize_for_transformers( - messages, role_names, default_role, custom_serializer, **serialize_kwargs) + messages, role_names, default_role, custom_serializer, **serialize_kwargs + ) @param.depends("_callback_state", watch=True) async def _update_input_disabled(self): diff --git a/panel/chat/langchain.py b/panel/chat/langchain.py index 2182e4b114..5aa31f2ca1 100644 --- a/panel/chat/langchain.py +++ b/panel/chat/langchain.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Union +from typing import Any try: from langchain.callbacks.base import BaseCallbackHandler @@ -108,7 +108,7 @@ def on_llm_end(self, response: LLMResult, *args, **kwargs): self._reset_active() return super().on_llm_end(response, *args, **kwargs) - def on_llm_error(self, error: Union[Exception, KeyboardInterrupt], *args, **kwargs): + def on_llm_error(self, error: Exception | KeyboardInterrupt, *args, **kwargs): return super().on_llm_error(error, *args, **kwargs) def on_agent_action(self, action: AgentAction, *args, **kwargs: Any) -> Any: @@ -130,7 +130,7 @@ def on_tool_end(self, output: str, *args, **kwargs): return super().on_tool_end(output, *args, **kwargs) def on_tool_error( - self, error: Union[Exception, KeyboardInterrupt], *args, **kwargs + self, error: Exception | KeyboardInterrupt, *args, **kwargs ): return super().on_tool_error(error, *args, **kwargs) @@ -146,7 +146,7 @@ def on_chain_end(self, outputs: dict[str, Any], *args, **kwargs): return super().on_chain_end(outputs, *args, **kwargs) def on_retriever_error( - self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any + self, error: Exception | KeyboardInterrupt, **kwargs: Any ) -> Any: """Run when Retriever errors.""" return super().on_retriever_error(error, **kwargs) diff --git a/panel/chat/message.py b/panel/chat/message.py index b69043cd16..395a70c81a 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -6,13 +6,14 @@ import datetime +from collections.abc import Callable from contextlib import ExitStack from dataclasses import dataclass from functools import partial from io import BytesIO from tempfile import NamedTemporaryFile from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union, + TYPE_CHECKING, Any, ClassVar, TypedDict, Union, ) from zoneinfo import ZoneInfo @@ -30,20 +31,26 @@ ) from ..pane.media import Audio, Video from ..param import ParamFunction -from ..viewable import Viewable +from ..viewable import ServableMixin, Viewable from ..widgets.base import Widget from .icon import ChatCopyIcon, ChatReactionIcons from .utils import ( avatar_lookup, build_avatar_pane, serialize_recursively, stream_to, ) +Avatar = Union[str, BytesIO, bytes, ImageBase] +AvatarDict = dict[str, Avatar] + if TYPE_CHECKING: from bokeh.document import Document from bokeh.model import Model from pyviz_comms import Comm -Avatar = Union[str, BytesIO, bytes, ImageBase] -AvatarDict = dict[str, Avatar] + class MessageParams(TypedDict, total=False): + avatar: Avatar + user: str + object: Any + value: Any USER_LOGO = "🧑" ASSISTANT_LOGO = "🤖" @@ -384,11 +391,11 @@ def _select_renderer( self, contents: Any, mime_type: str, - ): + ) -> tuple[Any, type[Pane] | Callable[..., Pane | ServableMixin]]: """ Determine the renderer to use based on the mime type. """ - renderer = _panel + renderer: type[Pane] | Callable[..., Pane | ServableMixin] = _panel if mime_type == "application/pdf": contents = self._exit_stack.enter_context(BytesIO(contents)) renderer = partial(PDF, embed=True) @@ -627,7 +634,7 @@ def stream(self, token: str, replace: bool = False): def update( self, - value: dict | ChatMessage | Any, + value: MessageParams | ChatMessage | Any, user: str | None = None, avatar: str | bytes | BytesIO | None = None, ): @@ -643,9 +650,9 @@ def update( avatar : str | bytes | BytesIO | None The avatar to use; overrides the message message's avatar if provided. """ - updates = {} + updates: MessageParams = {} if isinstance(value, dict): - updates.update(value) + updates.update(value) # type: ignore if user: updates["user"] = user if avatar: @@ -667,7 +674,7 @@ def update( self.param.update(**updates) 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._composite.select(selector) diff --git a/panel/chat/step.py b/panel/chat/step.py index 6b93ae4f6a..87e1ca6e6d 100644 --- a/panel/chat/step.py +++ b/panel/chat/step.py @@ -2,7 +2,8 @@ import traceback -from typing import ClassVar, Literal, Mapping +from collections.abc import Mapping +from typing import ClassVar, Literal import param diff --git a/panel/chat/utils.py b/panel/chat/utils.py index 32fce1bffa..5c061bb216 100644 --- a/panel/chat/utils.py +++ b/panel/chat/utils.py @@ -79,7 +79,7 @@ def build_avatar_pane( return avatar_pane -def stream_to(obj, token, replace=False, object_panel=None): +def stream_to(obj, token: str, replace: bool = False, object_panel: Viewable | None = None): """ Updates the message with the new token traversing the object to allow updating nested objects. When traversing a nested Panel diff --git a/panel/command/__init__.py b/panel/command/__init__.py index 0c3d728d50..7efd416cbf 100644 --- a/panel/command/__init__.py +++ b/panel/command/__init__.py @@ -11,12 +11,25 @@ from bokeh.util.strings import nice_join from .. import __version__ +from ..config import config from .bundle import Bundle from .compile import Compile from .convert import Convert from .oauth_secret import OAuthSecret from .serve import Serve +_DESCRIPTION = """\ +Found a Bug or Have a Feature Request? +Open an issue at: https://github.com/holoviz/panel/issues + +Have a Question? +Ask on our Discord chat server: https://discord.gg/rb6gPXbdAr + +Need Help? +Ask a question on our forum: https://discourse.holoviz.org + +For more information, see the documentation at: https://panel.holoviz.org """ + def transform_cmds(argv): """ @@ -49,80 +62,38 @@ def transform_cmds(argv): return transformed -def main(args=None): - """Mirrors bokeh CLI and adds additional Panel specific commands """ +def main(args: list[str] | None = None): from bokeh.command.subcommands import all as bokeh_commands - bokeh_commands = bokeh_commands + [OAuthSecret, Compile, Convert, Bundle] - - description = """\ -Found a Bug or Have a Feature Request? -Open an issue at: https://github.com/holoviz/panel/issues - -Have a Question? -Ask on our Discord chat server: https://discord.gg/rb6gPXbdAr - -Need Help? -Ask a question on our forum: https://discourse.holoviz.org - -For more information, see the documentation at: https://panel.holoviz.org """ - parser = argparse.ArgumentParser( prog="panel", epilog="See ' --help' to read about a specific subcommand.", - description=description, formatter_class=argparse.RawTextHelpFormatter + description=_DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter ) - parser.add_argument('-v', '--version', action='version', version=__version__) - subs = parser.add_subparsers(help="Sub-commands") - for cls in bokeh_commands: - if cls is BkServe: - subparser = subs.add_parser(Serve.name, help=Serve.help) - subcommand = Serve(parser=subparser) - subparser.set_defaults(invoke=subcommand.invoke) - elif cls is Compile: - subparser = subs.add_parser(Compile.name, help=Compile.help) - subcommand = Compile(parser=subparser) - subparser.set_defaults(invoke=subcommand.invoke) - elif cls is Convert: - subparser = subs.add_parser(Convert.name, help=Convert.help) - subcommand = Convert(parser=subparser) - subparser.set_defaults(invoke=subcommand.invoke) - elif cls is Bundle: - subparser = subs.add_parser(Bundle.name, help=Bundle.help) - subcommand = Bundle(parser=subparser) - subparser.set_defaults(invoke=subcommand.invoke) - else: - subs.add_parser(cls.name, help=cls.help) + commands = list(bokeh_commands) + for command in commands: + if command is not BkServe: + subs.add_parser(command.name, help=command.help) + for extra in (Bundle, Compile, Convert, OAuthSecret, Serve): + commands.append(extra) + subparser = subs.add_parser(extra.name, help=extra.help) + subcommand = extra(parser=subparser) + subparser.set_defaults(invoke=subcommand.invoke) if len(sys.argv) == 1: - all_commands = sorted([c.name for c in bokeh_commands]) + all_commands = sorted([c.name for c in commands]) die(f"ERROR: Must specify subcommand, one of: {nice_join(all_commands)}") - - if sys.argv[1] in ('--help', '-h'): - args = parser.parse_args(sys.argv[1:]) - args.invoke(args) - sys.exit() - - if len(sys.argv) > 1 and any(sys.argv[1] == c.name for c in bokeh_commands): + elif len(sys.argv) > 1 and any(sys.argv[1] == c.name for c in commands): sys.argv = transform_cmds(sys.argv) - if sys.argv[1] == 'serve': - args = parser.parse_args(sys.argv[1:]) + if sys.argv[1] in ('bundle', 'compile', 'convert', 'serve', 'help'): + parsed_args = parser.parse_args(sys.argv[1:]) try: - ret = args.invoke(args) + ret = parsed_args.invoke(parsed_args) except Exception as e: + if config.dev: + raise e die("ERROR: " + str(e)) - elif sys.argv[1] == 'oauth-secret': - ret = OAuthSecret(parser).invoke(args) - elif sys.argv[1] == 'convert': - args = parser.parse_args(sys.argv[1:]) - ret = Convert(parser).invoke(args) - elif sys.argv[1] == 'bundle': - args = parser.parse_args(sys.argv[1:]) - ret = Bundle(parser).invoke(args) - elif sys.argv[1] == 'compile': - args = parser.parse_args(sys.argv[1:]) - ret = Compile(parser).invoke(args) else: ret = bokeh_entry_point() else: diff --git a/panel/command/bundle.py b/panel/command/bundle.py index 4349bfe850..34c4d04a24 100644 --- a/panel/command/bundle.py +++ b/panel/command/bundle.py @@ -1,4 +1,4 @@ -from bokeh.command.subcommand import Subcommand +from bokeh.command.subcommand import Argument, Subcommand from ..compiler import ( bundle_icons, bundle_models, bundle_resource_urls, bundle_resources, @@ -17,35 +17,35 @@ class Bundle(Subcommand): help = "Bundle resources" args = ( - ('--all', dict( + ('--all', Argument( action = 'store_true', help = "Whether to bundle everything" )), - ('--resource-urls', dict( + ('--resource-urls', Argument( action = 'store_true', help = "Whether to bundle the global resources" )), - ('--templates', dict( + ('--templates', Argument( action = 'store_true', help = "Whether to bundle the template resources" )), - ('--themes', dict( + ('--themes', Argument( action = 'store_true', help = "Whether to bundle the theme resources" )), - ('--models', dict( + ('--models', Argument( action = 'store_true', help = "Whether to bundle the model resources" )), - ('--icons', dict( + ('--icons', Argument( action = 'store_true', help = "Whether to bundle icons." )), - ('--verbose', dict( + ('--verbose', Argument( action = 'store_true', help = "Whether to print progress" )), - ('--only-local', dict( + ('--only-local', Argument( action = 'store_true', help = "Whether to bundle only local resources" )), diff --git a/panel/command/compile.py b/panel/command/compile.py index 9b6421df83..5d189fd9ef 100644 --- a/panel/command/compile.py +++ b/panel/command/compile.py @@ -20,16 +20,16 @@ class Compile(Subcommand): help = "The Python modules to compile. May optionally define a single class.", default = None, )), - ('--build-dir', dict( + ('--build-dir', Argument( action = 'store', type = str, help = "Where to write the build directory." )), - ('--unminified', dict( + ('--unminified', Argument( action = 'store_true', help = "Whether to generate unminified output." )), - ('--verbose', dict( + ('--verbose', Argument( action = 'store_true', help = "Whether to show verbose output. Note when setting --outfile only the result will be printed to stdout." )), diff --git a/panel/command/convert.py b/panel/command/convert.py index 4ba0cfac38..affc02a384 100644 --- a/panel/command/convert.py +++ b/panel/command/convert.py @@ -1,8 +1,11 @@ import argparse import json +import os import pathlib import time +from typing import Literal, cast + from bokeh.command.subcommand import Argument, Subcommand from ..io.convert import convert_apps @@ -25,61 +28,61 @@ class Convert(Subcommand): help = "The app directories or scripts to serve (serve empty document if not specified)", default = None, )), - ('--exclude', dict( + ('--exclude', Argument( nargs = '*', help = "A list of files to exclude.", default = None )), - ('--to', dict( + ('--to', Argument( action = 'store', type = str, help = "The format to convert to, one of 'pyodide' (default), 'pyodide-worker', 'pyscript' or 'pyscript-worker'", default = 'pyodide' )), - ('--compiled', dict( + ('--compiled', Argument( default = False, - action = 'store_false', + action = 'store_true', help = "Whether to use the compiled and faster version of Pyodide." )), - ('--out', dict( + ('--out', Argument( action = 'store', type = str, help = "The directory to write the file to.", )), - ('--title', dict( + ('--title', Argument( action = 'store', type = str, help = "A custom title for the application(s).", )), - ('--skip-embed', dict( + ('--skip-embed', Argument( action = 'store_true', help = "Whether to skip embedding pre-rendered content in the converted file to display content while app is loading.", )), - ('--index', dict( + ('--index', Argument( action = 'store_true', help = "Whether to create an index if multiple files are served.", )), - ('--pwa', dict( + ('--pwa', Argument( action = 'store_true', help = "Whether to add files to serve applications as a Progressive Web App.", )), - ('--requirements', dict( + ('--requirements', Argument( nargs = '+', help = ( "Explicit requirements to add to the converted file, a single requirements.txt file or a " "JSON file containing requirements per app. By default requirements are inferred from the code." ) )), - ('--disable-http-patch', dict( + ('--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', dict( + ('--watch', Argument( action = 'store_true', help = "Watch the files" )), - ('--num-procs', dict( + ('--num-procs', Argument( action = 'store', type = int, default = 1, @@ -93,7 +96,7 @@ def invoke(self, args: argparse.Namespace) -> None: runtime = args.to.lower() if runtime not in self._targets: raise ValueError(f'Supported conversion targets include: {self._targets!r}') - requirements = args.requirements or 'auto' + requirements: list[str] | Literal['auto'] | os.PathLike = args.requirements or 'auto' if ( isinstance(requirements, list) and len(requirements) == 1 and @@ -101,7 +104,7 @@ def invoke(self, args: argparse.Namespace) -> None: ): req = requirements[0] if req.endswith('.txt'): - requirements = requirements[0] + requirements = pathlib.Path(requirements[0]) elif req.endswith('.json'): with open(req, encoding='utf-8') as req_file: requirements = json.load(req_file) @@ -115,7 +118,7 @@ def invoke(self, args: argparse.Namespace) -> None: elif p not in excluded: included.append(p) - prev_hashes = {} + prev_hashes: dict[pathlib.Path, int] = {} built = False while True: hashes = {f: hash(open(f).read()) for f in included} @@ -127,7 +130,8 @@ def invoke(self, args: argparse.Namespace) -> None: index = args.index and not built try: convert_apps( - files, dest_path=args.out, runtime=runtime, requirements=requirements, + cast(list[os.PathLike], files), + dest_path=args.out, runtime=runtime, requirements=requirements, prerender=not args.skip_embed, build_index=index, build_pwa=args.pwa, title=args.title, max_workers=args.num_procs, http_patch=not args.disable_http_patch, compiled=args.compiled, diff --git a/panel/command/serve.py b/panel/command/serve.py index ae4fe49838..5c2465ae41 100644 --- a/panel/command/serve.py +++ b/panel/command/serve.py @@ -19,6 +19,7 @@ DocumentLifecycleHandler, ) from bokeh.application.handlers.function import FunctionHandler +from bokeh.command.subcommand import Argument from bokeh.command.subcommands.serve import Serve as _BkServe from bokeh.command.util import build_single_handler_applications from bokeh.core.validation import silence @@ -87,190 +88,191 @@ def run_unload_hook(self): class Serve(_BkServe): - args = tuple((arg, arg_obj) for arg, arg_obj in _BkServe.args if arg != '--dev') + ( - ('--static-dirs', dict( + args = ( + tuple((arg, arg_obj) for arg, arg_obj in _BkServe.args if arg != '--dev') + ( + ('--static-dirs', Argument( metavar="KEY=VALUE", nargs='+', help=("Static directories to serve specified as key=value " "pairs mapping from URL route to static file directory.") )), - ('--basic-auth', dict( + ('--basic-auth', Argument( action = 'store', type = str, help = "Password or filepath to use with Basic Authentication." )), - ('--oauth-provider', dict( + ('--oauth-provider', Argument( action = 'store', type = str, help = "The OAuth2 provider to use." )), - ('--oauth-key', dict( + ('--oauth-key', Argument( action = 'store', type = str, help = "The OAuth2 key to use", )), - ('--oauth-secret', dict( + ('--oauth-secret', Argument( action = 'store', type = str, help = "The OAuth2 secret to use", )), - ('--oauth-redirect-uri', dict( + ('--oauth-redirect-uri', Argument( action = 'store', type = str, help = "The OAuth2 redirect URI", )), - ('--oauth-extra-params', dict( + ('--oauth-extra-params', Argument( action = 'store', type = str, help = "Additional parameters to use.", )), - ('--oauth-jwt-user', dict( + ('--oauth-jwt-user', Argument( action = 'store', type = str, help = "The key in the ID JWT token to consider the user.", )), - ('--oauth-encryption-key', dict( + ('--oauth-encryption-key', Argument( action = 'store', type = str, help = "A random string used to encode the user information." )), - ('--oauth-error-template', dict( + ('--oauth-error-template', Argument( action = 'store', type = str, help = "A random string used to encode the user information." )), - ('--oauth-expiry-days', dict( + ('--oauth-expiry-days', Argument( action = 'store', type = float, help = "Expiry off the OAuth cookie in number of days.", default = 1 )), - ('--oauth-refresh-tokens', dict( + ('--oauth-refresh-tokens', Argument( action = 'store_true', help = "Whether to automatically OAuth access tokens when they expire.", )), - ('--oauth-guest-endpoints', dict( + ('--oauth-guest-endpoints', Argument( action = 'store', nargs = '*', help = "List of endpoints that can be accessed as a guest without authenticating.", )), - ('--oauth-optional', dict( + ('--oauth-optional', Argument( action = 'store_true', help = ( "Whether the user will be forced to go through login flow " "or if they can access all applications as a guest." ) )), - ('--login-endpoint', dict( + ('--login-endpoint', Argument( action = 'store', type = str, help = "Endpoint to serve the authentication login page on." )), - ('--logout-endpoint', dict( + ('--logout-endpoint', Argument( action = 'store', type = str, help = "Endpoint to serve the authentication logout page on." )), - ('--auth-template', dict( + ('--auth-template', Argument( action = 'store', type = str, help = "Template to serve when user is unauthenticated." )), - ('--logout-template', dict( + ('--logout-template', Argument( action = 'store', type = str, help = "Template to serve logout page." )), - ('--basic-login-template', dict( + ('--basic-login-template', Argument( action = 'store', type = str, help = "Template to serve for Basic Authentication login page." )), - ('--rest-provider', dict( + ('--rest-provider', Argument( action = 'store', type = str, help = "The interface to use to serve REST API" )), - ('--rest-endpoint', dict( + ('--rest-endpoint', Argument( action = 'store', type = str, help = "Endpoint to store REST API on.", default = 'rest' )), - ('--rest-session-info', dict( + ('--rest-session-info', Argument( action = 'store_true', help = "Whether to serve session info on the REST API" )), - ('--session-history', dict( + ('--session-history', Argument( action = 'store', type = int, help = "The length of the session history to record.", default = 0 )), - ('--warm', dict( + ('--warm', Argument( action = 'store_true', help = "Whether to execute scripts on startup to warm up the server." )), - ('--admin', dict( + ('--admin', Argument( action = 'store_true', help = "Whether to add an admin panel." )), - ('--admin-endpoint', dict( + ('--admin-endpoint', Argument( action = 'store', type = str, help = "Name to use for the admin endpoint.", default = None )), - ('--admin-log-level', dict( + ('--admin-log-level', Argument( action = 'store', default = None, choices = ('debug', 'info', 'warning', 'error', 'critical'), help = "One of: debug (default), info, warning, error or critical", )), - ('--profiler', dict( + ('--profiler', Argument( action = 'store', type = str, help = "The profiler to use by default, e.g. pyinstrument, snakeviz or memray." )), - ('--dev', dict( + ('--dev', Argument( action = 'store_true', help = "Whether to enable dev mode. Equivalent to --autoreload." )), - ('--autoreload', dict( + ('--autoreload', Argument( action = 'store_true', help = "Whether to autoreload source when script changes. We recommend using --dev instead." )), - ('--num-threads', dict( + ('--num-threads', Argument( action = 'store', type = int, help = "Whether to start a thread pool which events are dispatched to.", default = None )), - ('--setup', dict( + ('--setup', Argument( action = 'store', type = str, help = "Path to a setup script to run before server starts. If --num-procs is enabled it will be run in each process after the server has started.", default = None )), - ('--liveness', dict( + ('--liveness', Argument( action = 'store_true', help = "Whether to add a liveness endpoint." )), - ('--liveness-endpoint', dict( + ('--liveness-endpoint', Argument( action = 'store', type = str, help = "The endpoint for the liveness API.", default = "liveness" )), - ('--reuse-sessions', dict( + ('--reuse-sessions', Argument( action = 'store_true', help = "Whether to reuse sessions when serving the initial request.", )), - ('--global-loading-spinner', dict( + ('--global-loading-spinner', Argument( action = 'store_true', help = "Whether to add a global loading spinner to the application(s).", )), - ) + )) # type: ignore[assignment] # Supported file extensions _extensions = ['.py', '.ipynb', '.md'] diff --git a/panel/config.py b/panel/config.py index 5e80fd251d..e9537ecb21 100644 --- a/panel/config.py +++ b/panel/config.py @@ -3,6 +3,8 @@ which provides convenient support for loading and configuring panel components. """ +from __future__ import annotations + import ast import copy import importlib @@ -13,6 +15,7 @@ from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, ClassVar from weakref import WeakKeyDictionary import param @@ -25,6 +28,9 @@ from .io.logging import panel_log_handler from .io.state import state +if TYPE_CHECKING: + from bokeh.document import Document + _LOCAL_DEV_VERSION = ( any(v in __version__ for v in ('post', 'dirty')) and not state._is_pyodide @@ -313,7 +319,7 @@ class _config(_base_config): The theme to apply to components.""") # Global parameters that are shared across all sessions - _globals = { + _globals: ClassVar[set[str]] = { 'admin_plugins', 'autoreload', 'comms', 'cookie_secret', 'nthreads', 'oauth_provider', 'oauth_expiry', 'oauth_key', 'oauth_secret', 'oauth_jwt_user', 'oauth_redirect_uri', @@ -324,7 +330,7 @@ class _config(_base_config): _truthy = ['True', 'true', '1', True, 1] - _session_config = WeakKeyDictionary() + _session_config: ClassVar[WeakKeyDictionary[Document, dict[str, Any]]] = WeakKeyDictionary() def __init__(self, **params): super().__init__(**params) @@ -653,9 +659,9 @@ class panel_extension(_pyviz_extension): will be using the `FastListTemplate`. """ - _loaded = False + _loaded: bool = False - _imports = { + _imports: ClassVar[dict[str, str]] = { 'ace': 'panel.models.ace', 'codeeditor': 'panel.models.ace', 'deckgl': 'panel.models.deckgl', @@ -678,7 +684,7 @@ class panel_extension(_pyviz_extension): # Check whether these are loaded before rendering (if any item # in the list is available the extension will be confidered as # loaded) - _globals = { + _globals: ClassVar[dict[str, list[str]]] = { 'deckgl': ['deck'], 'echarts': ['echarts'], 'filedropper': ['FilePond'], @@ -695,9 +701,9 @@ class panel_extension(_pyviz_extension): 'vtk': ['vtk'] } - _loaded_extensions = [] + _loaded_extensions: list[str] = [] - _comms_detected_before = False + _comms_detected_before: bool = False def __call__(self, *args, **params): from bokeh.core.has_props import _default_resolver diff --git a/panel/custom.py b/panel/custom.py index 136c8bc840..a820e6d36d 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -10,9 +10,10 @@ import textwrap from collections import defaultdict +from collections.abc import Callable, Mapping from functools import partial from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Literal, Mapping, Optional, + TYPE_CHECKING, Any, ClassVar, Literal, ) import param @@ -43,7 +44,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, ...]]] @@ -127,7 +128,6 @@ def _create__view(self): view = ParamMethod(self.__panel__, lazy=True) else: view = panel(self.__panel__()) - self._view__ = view params = view.param.values() overrides, sync = {}, {} for p in Layoutable.param: @@ -139,20 +139,21 @@ def _create__view(self): self.param.update(overrides) with param.parameterized._syncing(view, list(sync)): view.param.update(sync) + return view 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: if self._view__ is None: - self._create__view() + self._view__ = self._create__view() model = self._view__._get_model(doc, root, parent, comm) root = model if root is None else root self._models[root.ref['id']] = (model, parent) 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 +251,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 +287,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 +309,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 +323,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 +347,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 +385,10 @@ def _update_esm(self): self._apply_update({}, {'esm': esm}, model, ref) @property - def _linked_properties(self) -> list[str]: - return [p for p in self._data_model.properties() if p not in ('js_property_callbacks',)] + def _linked_properties(self) -> tuple[str, ...]: + return tuple(p for p in self._data_model.properties() if p != '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 +414,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 +438,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 +453,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] @@ -468,21 +473,22 @@ def _setup_autoreload(self): state.execute(self._watch_esm) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - model = self._bokeh_model(**self._get_properties(doc)) + props = self._get_properties(doc) + model = self._bokeh_model(**props) 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._link_props(props['data'], self._linked_properties, doc, root, comm) self._register_events('dom_event', 'data_event', model=model, doc=doc, comm=comm) self._setup_autoreload() return model - def _process_event(self, event: 'Event') -> None: + def _process_event(self, event: Event) -> None: if isinstance(event, DataEvent): for cb in self._msg__callbacks: state.execute(partial(cb, event), schedule=False) @@ -497,7 +503,7 @@ def _process_event(self, event: 'Event') -> None: def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: model_msg, data_msg = {}, {} for prop, v in list(msg.items()): @@ -523,7 +529,7 @@ def _update_model( self._set_on_model(model_msg, root, model) self._set_on_model(data_msg, root, model.data) - def _handle_msg(self, data: any) -> None: + def _handle_msg(self, data: Any) -> None: """ Message handler for messages sent from the frontend using the `model.send_msg` API. @@ -534,7 +540,7 @@ def _handle_msg(self, data: any) -> None: Data received from the frontend. """ - def _send_msg(self, data: any) -> None: + def _send_msg(self, data: Any) -> None: """ Sends data to the frontend which can be observed on the frontend with the `model.on_msg("msg:custom", callback)` API. @@ -662,10 +668,10 @@ class CounterButton(pn.custom.ReactComponent): _react_version = '18.3.1' - @classproperty + @classproperty # type: ignore def _exports__(cls) -> ExportSpec: imports = cls._importmap.get('imports', {}) - exports = { + exports: dict[str, list[str | tuple[str, ...]]] = { "react": ["*React"], "react-dom/client": [("createRoot",)] } diff --git a/panel/entry_points.py b/panel/entry_points.py index b37a997754..1dfccf3325 100644 --- a/panel/entry_points.py +++ b/panel/entry_points.py @@ -6,7 +6,7 @@ import importlib.metadata -from typing import Iterator +from collections.abc import Iterator def entry_points_for(group: str) -> Iterator[importlib.metadata.EntryPoint]: @@ -16,7 +16,7 @@ def entry_points_for(group: str) -> Iterator[importlib.metadata.EntryPoint]: # Load-time selection requires Python >= 3.10 or importlib_metadata >= 3.6, # so we'll retain this fallback logic for some time to come. See also # https://importlib-metadata.readthedocs.io/en/latest/using.html - eps = importlib.metadata.entry_points().get(group, []) + eps = importlib.metadata.entry_points().get(group, []) # type: ignore[arg-type] yield from eps 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 f256281e6b..9df42317ec 100644 --- a/panel/io/application.py +++ b/panel/io/application.py @@ -7,11 +7,10 @@ import logging import os +from collections.abc import Callable, Sequence from functools import partial from types import FunctionType, MethodType -from typing import ( - TYPE_CHECKING, Any, Callable, Mapping, -) +from typing import TYPE_CHECKING, Any, TypeAlias from urllib.parse import urljoin import bokeh.command.util @@ -40,15 +39,15 @@ from ..viewable import Viewable, Viewer from .location import Location - TViewable = Viewable | Viewer | BaseTemplate - TViewableFuncOrPath = TViewable | Callable[[], TViewable] | os.PathLike | str + TViewable: TypeAlias = Viewable | Viewer | BaseTemplate + TViewableFuncOrPath: TypeAlias = TViewable | Callable[[], TViewable] | os.PathLike | str logger = logging.getLogger('panel.io.application') 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 +172,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 +189,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 +222,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 +251,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 +263,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 custom_handler in (custom_handlers or ()): + new_app = custom_handler(slug, app) if app is not None: break else: diff --git a/panel/io/browser.py b/panel/io/browser.py index 66fad402aa..ffce83c817 100644 --- a/panel/io/browser.py +++ b/panel/io/browser.py @@ -4,7 +4,8 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Mapping +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param # type: ignore @@ -65,7 +66,7 @@ def _get_model( def get_root( self, doc: Document | None = None, comm: Comm | None = None, preprocess: bool = True - ) -> 'Model': + ) -> Model: doc = create_doc_if_none_exists(doc) root = self._get_model(doc, comm=comm) ref = root.ref['id'] diff --git a/panel/io/cache.py b/panel/io/cache.py index db51f41572..2a3be882e5 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -15,12 +15,11 @@ import threading import time import unittest.mock -import weakref +from collections.abc import Awaitable, Callable, Hashable from contextlib import contextmanager from typing import ( - TYPE_CHECKING, Any, Callable, Hashable, Literal, ParamSpec, Protocol, - TypeVar, overload, + TYPE_CHECKING, Any, Literal, ParamSpec, Protocol, TypeVar, cast, overload, ) import param @@ -50,8 +49,6 @@ def clear(self, func_hashes: list[str | None]=[None]) -> None: _HASH_MAP: dict[Hashable, str] = {} -_HASH_STACKS = weakref.WeakKeyDictionary() - _INDETERMINATE = type('INDETERMINATE', (object,), {})() _NATIVE_TYPES = ( @@ -92,34 +89,34 @@ def _get_fqn(obj): name = the_type.__qualname__ return f"{module}.{name}" -def _int_to_bytes(i): +def _int_to_bytes(i: int) -> bytes: num_bytes = (i.bit_length() + 8) // 8 return i.to_bytes(num_bytes, "little", signed=True) -def _is_native(obj): +def _is_native(obj: Any) -> bool: return isinstance(obj, _NATIVE_TYPES) -def _is_native_tuple(obj): +def _is_native_tuple(obj: Any) -> bool: return isinstance(obj, tuple) and all(_is_native_tuple(v) for v in obj) -def _container_hash(obj): +def _container_hash(obj: Any) -> bytes: h = hashlib.new("md5") h.update(_generate_hash(f'__{type(obj).__name__}')) for item in (obj.items() if isinstance(obj, dict) else obj): h.update(_generate_hash(item)) return h.digest() -def _slice_hash(x): +def _slice_hash(x: slice) -> bytes: return _container_hash([x.start, x.step, x.stop]) -def _partial_hash(obj): +def _partial_hash(obj: Any) -> bytes: h = hashlib.new("md5") h.update(_generate_hash(obj.args)) h.update(_generate_hash(obj.func)) h.update(_generate_hash(obj.keywords)) return h.digest() -def _pandas_hash(obj): +def _pandas_hash(obj: Any) -> bytes: import pandas as pd if not isinstance(obj, (pd.Series, pd.DataFrame)): @@ -203,7 +200,7 @@ def _io_hash(obj): h.update(_generate_hash(obj.getvalue())) return h.digest() -_hash_funcs = { +_hash_funcs: dict[str | type[Any] | tuple[type, ...] | Callable[[Any], bool], bytes | Callable[[Any], bytes]] = { # Types int : _int_to_bytes, str : lambda obj: obj.encode(), @@ -217,7 +214,7 @@ def _io_hash(obj): functools.partial : _partial_hash, unittest.mock.Mock : lambda obj: _int_to_bytes(id(obj)), (io.StringIO, io.BytesIO): _io_hash, - dt.date : lambda obj: f'{type(obj).__name__}{obj}'.encode('utf-8'), + dt.date : lambda obj: f'{type(obj).__name__}{obj}'.encode(), # Fully qualified type strings 'numpy.ndarray' : _numpy_hash, 'pandas.core.series.Series' : _pandas_hash, @@ -522,7 +519,7 @@ async def wrapped_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: ret, ts, count, _ = func_cache[hash_value] func_cache[hash_value] = (ret, ts, count+1, time) else: - ret = await func(*args, **kwargs) + ret = await cast(Awaitable[Any], func(*args, **kwargs)) with lock: func_cache[hash_value] = (ret, time, 0, time) return ret @@ -565,7 +562,7 @@ def server_clear(session_context, clear=clear): except AttributeError: pass - return wrapped_func + return wrapped_func # type: ignore def is_equal(value, other)->bool: """Returns True if value and other are equal diff --git a/panel/io/compile.py b/panel/io/compile.py index 6e8beba647..2455c560ac 100644 --- a/panel/io/compile.py +++ b/panel/io/compile.py @@ -13,14 +13,15 @@ from collections import defaultdict from contextlib import contextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from bokeh.application.handlers.code_runner import CodeRunner from ..custom import ReactComponent, ReactiveESM if TYPE_CHECKING: - from .custom import ExportSpec + from ..custom import ExportSpec + GREEN, RED, RESET = "\033[0;32m", "\033[0;31m", "\033[0m" @@ -52,7 +53,7 @@ def setup_build_dir(build_dir: str | os.PathLike | None = None): temp_dir = pathlib.Path(build_dir).absolute() temp_dir.mkdir(parents=True, exist_ok=True) else: - temp_dir = tempfile.mkdtemp() + temp_dir = pathlib.Path(tempfile.mkdtemp()) try: os.chdir(temp_dir) yield temp_dir @@ -64,7 +65,7 @@ def setup_build_dir(build_dir: str | os.PathLike | None = None): def check_cli_tool(tool_name): try: - result = subprocess.run([tool_name, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run([tool_name, '--version'], capture_output=True) if result.returncode == 0: return True else: @@ -73,7 +74,7 @@ def check_cli_tool(tool_name): return False -def find_module_bundles(module_spec: str) -> dict[pathlib.PurePath, list[ReactiveESM]]: +def find_module_bundles(module_spec: str) -> dict[pathlib.Path, list[ReactiveESM]]: """ Takes module specifications and extracts a set of components to bundle. @@ -106,12 +107,13 @@ def find_module_bundles(module_spec: str) -> dict[pathlib.PurePath, list[Reactiv f'you provided the right module{cls_error}.' ) if module in sys.modules: - module_path = sys.modules[module].__file__ + module_file = sys.modules[module].__file__ else: - module_path = module + module_file = module + assert module_file is not None bundles = defaultdict(list) - module_path = pathlib.Path(module_path).parent + module_path = pathlib.Path(module_file).parent for component in components: if component._bundle: bundle_path = component._bundle @@ -119,14 +121,14 @@ def find_module_bundles(module_spec: str) -> dict[pathlib.PurePath, list[Reactiv path = (module_path / bundle_path).absolute() else: path = bundle_path.absolute() - bundles[str(path)].append(component) + bundles[path].append(component) elif len(components) > 1 and not classes: component_module = module_name or component.__module__ bundles[module_path / f'{component_module}.bundle.js'].append(component) else: bundles[component._module_path / f'{component.__name__}.bundle.js'].append(component) - return bundles + return dict(bundles) def find_components(module_or_file: str | os.PathLike, classes: list[str] | None = None) -> list[type[ReactiveESM]]: @@ -145,19 +147,20 @@ def find_components(module_or_file: str | os.PathLike, classes: list[str] | None ------- List of ReactiveESM components defined in the module. """ - py_file = module_or_file.endswith('.py') + py_file = str(module_or_file).endswith('.py') if py_file: path_obj = pathlib.Path(module_or_file) source = path_obj.read_text(encoding='utf-8') runner = CodeRunner(source, module_or_file, []) module = runner.new_module() + assert module is not None runner.run(module) if runner.error: raise RuntimeError( f'Compilation failed because supplied module errored on import:\n\n{runner.error}' ) else: - module = importlib.import_module(module_or_file) + module = importlib.import_module(str(module_or_file)) classes = classes or [] components = [] for v in module.__dict__.values(): @@ -170,14 +173,14 @@ def find_components(module_or_file: str | os.PathLike, classes: list[str] | None if py_file: v.__path__ = path_obj.parent.absolute() components.append(v) - not_found = {cls for cls in classes if '*' not in cls} - set(c.__name__ for c in components) + not_found = {cls for cls in classes if '*' not in cls} - {c.__name__ for c in components} if classes and not_found: clss = ', '.join(map(repr, not_found)) raise ValueError(f'{clss} class(es) not found in {module_or_file!r}.') return components -def packages_from_code(esm_code: str) -> dict[str, str]: +def packages_from_code(esm_code: str) -> tuple[str, dict[str, str]]: """ Extracts package version definitions from ESM code. @@ -205,7 +208,7 @@ def packages_from_code(esm_code: str) -> dict[str, str]: return esm_code, packages -def replace_imports(esm_code: str, replacements: dict[str, str]) -> dict[str, str]: +def replace_imports(esm_code: str, replacements: dict[str, str]) -> str: """ Replaces imports in the code which may be aliases with the actual package names. @@ -239,7 +242,7 @@ def replace_match(match): return modified_code -def packages_from_importmap(esm_code: str, imports: dict[str, str]) -> dict[str, str]: +def packages_from_importmap(esm_code: str, imports: dict[str, str]) -> tuple[str, dict[str, str]]: """ Extracts package version definitions from an import map. @@ -270,7 +273,7 @@ def packages_from_importmap(esm_code: str, imports: dict[str, str]) -> dict[str, return esm_code, dependencies -def extract_dependencies(component: type[ReactiveESM]) -> tuple[str, dict[str, any]]: +def extract_dependencies(component: type[ReactiveESM]) -> tuple[str, dict[str, Any]]: """ Extracts dependencies from a ReactiveESM component by parsing its importmap and the associated code and replaces URL import @@ -345,7 +348,7 @@ def generate_index(imports: str, exports: list[str], export_spec: ExportSpec): def generate_project( components: list[type[ReactiveESM]], path: str | os.PathLike, - project_config: dict[str, any] = None + project_config: dict[str, Any] | None = None ): """ Converts a set of ESM components into a Javascript project with @@ -353,7 +356,8 @@ def generate_project( """ path = pathlib.Path(path) component_names = [] - dependencies, export_spec = {}, {} + dependencies = {} + export_spec: ExportSpec = {} index = '' for component in components: name = component.__name__ @@ -391,11 +395,11 @@ def generate_project( def compile_components( components: list[type[ReactiveESM]], - build_dir: str | os.PathLike = None, - outfile: str | os.PathLike = None, + build_dir: str | os.PathLike | None = None, + outfile: str | os.PathLike | None = None, minify: bool = True, verbose: bool = True -) -> str | None: +) -> int | str | None: """ Compiles a list of ReactiveESM components into a single JavaScript bundle including their Javascript dependencies. @@ -434,8 +438,8 @@ def compile_components( ) out = str(pathlib.Path(outfile).absolute()) if outfile else None - with setup_build_dir(build_dir) as build_dir: - generate_project(components, build_dir) + with setup_build_dir(build_dir) as out_dir: + generate_project(components, out_dir) extra_args = [] if verbose: extra_args.append('--log-level=debug') diff --git a/panel/io/convert.py b/panel/io/convert.py index 3a909c2184..747f36ca78 100644 --- a/panel/io/convert.py +++ b/panel/io/convert.py @@ -7,6 +7,7 @@ import pathlib import uuid +from collections.abc import Sequence from typing import IO, Any, Literal import bokeh @@ -56,7 +57,7 @@ PYODIDE_PYC_JS = f'' LOCAL_PREFIX = './' -MINIMUM_VERSIONS = {} +MINIMUM_VERSIONS: dict[str, str] = {} ICON_DIR = DIST_DIR / 'images' PWA_IMAGES = [ @@ -133,7 +134,7 @@ def make_index(files, title=None, manifest=True): favicon=favicon, title=title, PANEL_CDN=CDN_DIST ) -def build_pwa_manifest(files, title=None, **kwargs): +def build_pwa_manifest(files, title=None, **kwargs) -> str: if len(files) > 1: title = title or 'Panel Applications' path = 'index.html' @@ -148,7 +149,7 @@ def build_pwa_manifest(files, title=None, **kwargs): 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', @@ -159,7 +160,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. @@ -168,7 +169,7 @@ def script_to_html( --------- filename: str | Path | IO The filename of the Panel/Bokeh application to convert. - requirements: 'auto' | List[str] + requirements: 'auto' | List[str] | os.PathLike The list of requirements to include (in addition to Panel). js_resources: 'auto' | List[str] The list of JS resources to include in the exported HTML. @@ -200,7 +201,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) @@ -214,19 +215,23 @@ def script_to_html( ) if requirements == 'auto': - requirements = find_requirements(source) + requirement_list = find_requirements(source) elif isinstance(requirements, str) and pathlib.Path(requirements).is_file(): - requirements = pathlib.Path(requirements).read_text(encoding='utf-8').splitlines() + requirement_list = pathlib.Path(requirements).read_text(encoding='utf-8').splitlines() try: from packaging.requirements import Requirement - requirements = [ - r2 for r in requirements + requirement_list = [ + r2 for r in requirement_list if (r2 := r.split("#")[0].strip()) and Requirement(r2) ] except Exception as e: raise ValueError( f'Requirements parser raised following error: {e}' ) from e + elif isinstance(requirements, list): + requirement_list = requirements + else: + raise ValueError(f'Could not resolve requirements file {requirements}') # Environment if panel_version == 'local': @@ -243,7 +248,7 @@ def script_to_html( if http_patch: base_reqs.append('pyodide-http==0.2.1') reqs = base_reqs + [ - req for req in requirements if req not in ('panel', 'bokeh') + req for req in requirement_list if req not in ('panel', 'bokeh') ] for name, min_version in MINIMUM_VERSIONS.items(): if any(name in req for req in reqs): @@ -414,11 +419,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}.') @@ -426,8 +429,8 @@ def convert_app( def _convert_process_pool( - apps: list[str], - dest_path: str | None = None, + apps: Sequence[str | os.PathLike], + dest_path: os.PathLike | str | None = None, max_workers: int = 4, requirements: list[str] | Literal['auto'] | os.PathLike = 'auto', **kwargs @@ -460,7 +463,7 @@ def _convert_process_pool( return files def convert_apps( - apps: str | os.PathLike | list[str | os.PathLike], + apps: str | os.PathLike | Sequence[str | os.PathLike], dest_path: str | os.PathLike | None = None, title: str | None = None, runtime: Runtimes = 'pyodide-worker', @@ -543,18 +546,25 @@ def convert_apps( app_requirements = requirements kwargs = { - 'requirements': app_requirements, '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, **kwargs) for app in apps) + files = { + app: convert_app(app, dest_path, requirements=app_requirements, **kwargs) # type: ignore + for app in apps + } else: files = _convert_process_pool( - apps, dest_path, max_workers=max_workers, **kwargs + apps, dest_path, max_workers=max_workers, requirements=app_requirements, **kwargs ) if build_index and len(files) >= 1: 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/django.py b/panel/io/django.py index 6fb1a23d08..bf6a103819 100644 --- a/panel/io/django.py +++ b/panel/io/django.py @@ -4,7 +4,7 @@ try: from bokeh_django.consumers import AutoloadJsConsumer, DocConsumer except Exception: - from bokeh.server.django.consumers import AutoloadJsConsumer, DocConsumer + from bokeh.server.django.consumers import AutoloadJsConsumer, DocConsumer # type: ignore from ..util import edit_readonly from .resources import Resources diff --git a/panel/io/document.py b/panel/io/document.py index 290e713d99..9fd866ddd4 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -12,19 +12,15 @@ import time import weakref +from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from functools import partial, wraps -from typing import ( - TYPE_CHECKING, Any, Callable, Iterator, -) +from typing import TYPE_CHECKING, Any +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, MessageSentEvent, ModelChangedEvent, -) +from bokeh.document.events import DocumentChangedEvent, DocumentPatchedEvent from bokeh.model.util import visit_immediate_value_references from bokeh.models import CustomJS @@ -34,8 +30,11 @@ from .state import curdoc_locked, state if TYPE_CHECKING: + from asyncio.futures import Future + from bokeh.core.enums import HoldPolicyType from bokeh.core.has_props import HasProps + from bokeh.document import Document from bokeh.protocol.message import Message from bokeh.server.connection import ServerConnection from pyviz_comms import Comm @@ -46,19 +45,15 @@ # Private API #--------------------------------------------------------------------- -DISPATCH_EVENTS = ( - ColumnDataChangedEvent, ColumnsPatchedEvent, ColumnsStreamedEvent, - ModelChangedEvent, MessageSentEvent -) GC_DEBOUNCE = 5 -_WRITE_FUTURES = weakref.WeakKeyDictionary() -_WRITE_MSGS = weakref.WeakKeyDictionary() -_WRITE_BLOCK = weakref.WeakKeyDictionary() +_WRITE_FUTURES: WeakKeyDictionary[Document, list[Future]] = WeakKeyDictionary() +_WRITE_MSGS: WeakKeyDictionary[Document, dict[ServerConnection, list[Message]]] = WeakKeyDictionary() +_WRITE_BLOCK: WeakKeyDictionary[Document, bool] = WeakKeyDictionary() _panel_last_cleanup = None -_write_tasks = [] +_write_tasks: list[asyncio.Task] = [] -extra_socket_handlers = {} +extra_socket_handlers: dict[type, Callable[[Any], None]] = {} @dataclasses.dataclass class Request: @@ -280,11 +275,11 @@ def retrigger_events(doc: Document, events: list[DocumentChangedEvent]): def write_events( doc: Document, connections: list[ServerConnection], - events: list[DocumentChangedEvent] + events: list[DocumentPatchedEvent] ): from tornado.websocket import WebSocketHandler - futures = [] + futures: list[Future] = [] for conn in connections: if isinstance(conn._socket, WebSocketHandler): futures += dispatch_tornado(conn, events) @@ -306,7 +301,7 @@ def write_events( def schedule_write_events( doc: Document, connections: list[ServerConnection], - events: list[DocumentChangedEvent] + events: list[DocumentPatchedEvent] ): # Set up write locks _WRITE_BLOCK[doc] = True @@ -385,16 +380,19 @@ def wrapper(*args, **kw): def dispatch_tornado( conn: ServerConnection, - events: list[DocumentChangedEvent] | None = None, + events: list[DocumentPatchedEvent] | None = None, msg: Message | None = None -): +) -> Sequence[Future]: from tornado.websocket import WebSocketHandler socket = conn._socket ws_conn = getattr(socket, 'ws_connection', False) if not ws_conn or ws_conn.is_closing(): # type: ignore return [] - if msg is None: - msg = conn.protocol.create('PATCH-DOC', events) + elif msg is None: + if events: + msg = conn.protocol.create('PATCH-DOC', events) + else: + return [] futures = [ WebSocketHandler.write_message(socket, msg.header_json), WebSocketHandler.write_message(socket, msg.metadata_json), @@ -411,12 +409,17 @@ def dispatch_tornado( def dispatch_django( conn: ServerConnection, - events: list[DocumentChangedEvent] | None = None, + events: list[DocumentPatchedEvent] | None = None, msg: Message | None = None -): +) -> Sequence[Future]: socket = conn._socket if msg is None: - msg = conn.protocol.create('PATCH-DOC', events) + return [] + elif msg is None: + if events: + msg = conn.protocol.create('PATCH-DOC', events) + else: + return futures = [ socket.send(text_data=msg.header_json), socket.send(text_data=msg.metadata_json), @@ -488,7 +491,7 @@ 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) @@ -506,8 +509,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..969e129616 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 @@ -29,6 +27,9 @@ raise ImportError(msg) from None if TYPE_CHECKING: + from bokeh.application import Application as BkApplication + from bokeh.document.events import DocumentPatchedEvent + from bokeh.protocol.message import Message from uvicorn import Server from .application import TViewableFuncOrPath @@ -41,7 +42,7 @@ DocHandler.render_session = server_html_page_for_session -def dispatch_fastapi(conn, events=None, msg=None): +def dispatch_fastapi(conn, events: list[DocumentPatchedEvent] | None = None, msg: Message | None = None): if msg is None: msg = conn.protocol.create("PATCH-DOC", events) return [conn._socket.send_message(msg)] @@ -49,7 +50,7 @@ def dispatch_fastapi(conn, events=None, msg=None): extra_socket_handlers[WSHandler] = dispatch_fastapi -def add_liveness_handler(app, endpoint, applications): +def add_liveness_handler(app, endpoint: str, applications: dict[str, BkApplication]): @app.get(endpoint, response_model=dict[str, bool]) async def liveness_handler(request: Request, endpoint: str | None = Query(None)): if endpoint is not None: @@ -78,7 +79,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 +190,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, @@ -253,6 +254,8 @@ def get_server( if loop: config_kwargs['loop'] = loop asyncio.set_event_loop(loop) + if port: + config_kwargs['port'] = port server_id = kwargs.pop('server_id', uuid.uuid4().hex) application = add_applications( panel, title=title, location=location, admin=admin, **kwargs @@ -268,8 +271,7 @@ def show_callback(): url = f"http://{address_string}:{config.port}{prefix}" from bokeh.util.browser import view view(url, new='tab') - - config = uvicorn.Config(application.app, port=port, **config_kwargs) + config = uvicorn.Config(application.app, **config_kwargs) server = uvicorn.Server(config) state._servers[server_id] = (server, panel, []) @@ -288,7 +290,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 8b41319c1b..fbe5dd6842 100644 --- a/panel/io/handlers.py +++ b/panel/io/handlers.py @@ -11,9 +11,10 @@ import traceback import urllib.parse as urlparse +from collections.abc import Callable, Iterator from contextlib import contextmanager from types import ModuleType -from typing import IO, Any, Callable +from typing import IO, TYPE_CHECKING, Any import bokeh.command.util @@ -31,6 +32,9 @@ from .reload import record_modules from .state import state +if TYPE_CHECKING: + from nbformat import NotebookNode + log = logging.getLogger('panel.io.handlers') CELL_DISPLAY = [] @@ -51,7 +55,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: @@ -102,7 +106,7 @@ def extract_code( inblock = False block_opener = None title = None - markdown = [] + markdown: list[str] = [] out = [] while True: line = filehandle.readline() @@ -261,7 +265,7 @@ def post_check(): raise RuntimeError(f"{handler._origin} at '{handler._runner.path}' replaced the output document") try: - state._launching.append(doc) + state._launching.add(doc) with _monkeypatch_io(handler._loggers): with patch_curdoc(doc): with profile_ctx(config.profiler) as sessions: @@ -297,7 +301,10 @@ def post_check(): if old_doc is not None: bk_set_curdoc(old_doc) -def parse_notebook(filename: str | os.PathLike | IO, preamble: list[str] | None = None): +def parse_notebook( + filename: str | os.PathLike | IO, + preamble: list[str] | None = None +) -> tuple[NotebookNode, str, dict[str, Any]]: """ Parses a notebook on disk and returns a script. @@ -320,7 +327,7 @@ def parse_notebook(filename: str | os.PathLike | IO, preamble: list[str] | None 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). @@ -376,8 +383,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 @@ -429,10 +436,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: @@ -445,7 +464,7 @@ def url_path(self) -> str | None: # TODO should fix invalid URL characters return '/' + os.path.splitext(os.path.basename(self._runner.path))[0] - def modify_document(self, doc: 'Document'): + def modify_document(self, doc: Document): if config.autoreload: path = self._runner.path argv = self._runner._argv @@ -468,7 +487,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): @@ -492,7 +511,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): @@ -720,4 +739,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/ipywidget.py b/panel/io/ipywidget.py index 7cf01502c3..2129696513 100644 --- a/panel/io/ipywidget.py +++ b/panel/io/ipywidget.py @@ -17,7 +17,7 @@ from ipywidgets.widgets.widget import _remove_buffers # Stop ipywidgets_bokeh from patching the kernel -ipykernel.kernelbase.Kernel._instance = '' +ipykernel.kernelbase.Kernel._instance = '' # type: ignore from ipywidgets_bokeh.kernel import ( BokehKernel, SessionWebsocket, WebsocketStream, @@ -33,7 +33,7 @@ try: from ipykernel.comm.comm import BaseComm as _IPyComm except Exception: - from ipykernel.comm.comm import Comm as _IPyComm + from ipykernel.comm.comm import Comm as _IPyComm # type: ignore try: # Support for ipywidgets>=8.0.5 @@ -46,7 +46,7 @@ def publish_msg(self, *args, **kwargs): pass comm.create_comm = lambda *args, **kwargs: TempComm(target_name='panel-temp-comm', primary=False) except Exception: - comm = None + comm = None # type: ignore def _get_kernel(cls=None, doc=None): doc = doc or state.curdoc @@ -192,7 +192,7 @@ class PanelKernel(Kernel): implementation_version = __version__ banner = 'banner' - shell_stream = Any(ShellStream(), allow_none=True) + shell_stream = Any(ShellStream(), allow_none=True) # type: ignore def __init__(self, key=None, document=None): super().__init__() @@ -233,5 +233,5 @@ def wrapper(*args, **kwargs): # Patch kernel and widget objects _ORIG_KERNEL = ipykernel.kernelbase.Kernel._instance if isinstance(ipykernel.kernelbase.Kernel._instance, (BokehKernel, str)): - ipykernel.kernelbase.Kernel._instance = classproperty(_get_kernel) + ipykernel.kernelbase.Kernel._instance = classproperty(_get_kernel) # type: ignore Widget.on_widget_constructed(_on_widget_constructed) diff --git a/panel/io/jupyter_executor.py b/panel/io/jupyter_executor.py index d8c9758f3e..4e00cd519e 100644 --- a/panel/io/jupyter_executor.py +++ b/panel/io/jupyter_executor.py @@ -6,7 +6,7 @@ import weakref from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any import tornado @@ -51,7 +51,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 +71,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 +144,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, None, doc # type: ignore ) # 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,8 +180,8 @@ def _create_server_session(self) -> ServerSession: session_context._set_session(session) return session, runner.error_detail - async def write_message( - self, message: Union[bytes, str, dict[str, Any]], + async def write_message( # type: ignore + self, message: bytes | str | dict[str, Any], binary: bool = False, locked: bool = True ) -> None: metadata = {'binary': binary} @@ -197,7 +197,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..a4bd36bcdf 100644 --- a/panel/io/jupyter_server_extension.py +++ b/panel/io/jupyter_server_extension.py @@ -33,8 +33,9 @@ import textwrap import time +from collections.abc import Awaitable from queue import Empty -from typing import Any, Awaitable +from typing import Any from urllib.parse import urljoin import tornado @@ -118,7 +119,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 +322,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 +351,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 +397,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 +445,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/location.py b/panel/io/location.py index 0fcafc4114..946463f555 100644 --- a/panel/io/location.py +++ b/panel/io/location.py @@ -6,9 +6,8 @@ import json import urllib.parse as urlparse -from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, -) +from collections.abc import Callable, Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -125,9 +124,9 @@ def __init__(self, **params): self.param.watch(self._update_synced, ['search']) 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: model = _BkLocation(**self._process_param_change(self._init_params())) root = root or model self._models[root.ref['id']] = (model, parent) @@ -135,9 +134,9 @@ def _get_model( return model 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 = self._get_model(doc, comm=comm) ref = root.ref['id'] @@ -193,7 +192,7 @@ def _update_synced(self, event: param.parameterized.Event = None) -> None: on_error(mapped) def _update_query( - self, *events: param.parameterized.Event, query: Optional[dict[str, Any]] = None + self, *events: param.parameterized.Event, query: dict[str, Any] | None = None ) -> None: if self._syncing: return @@ -226,8 +225,8 @@ def update_query(self, **kwargs: Mapping[str, Any]) -> None: self.search = '?' + urlparse.urlencode(query) def sync( - self, parameterized: param.Parameterized, parameters: Optional[list[str] | dict[str, str]] = None, - on_error: Optional[Callable[[dict[str, Any]], None]] = None + self, parameterized: param.Parameterized, parameters: list[str] | dict[str, str] | None = None, + on_error: Callable[[dict[str, Any]], None] | None = None ) -> None: """ Syncs the parameters of a Parameterized object with the query @@ -268,7 +267,7 @@ def sync( query[name] = v self._update_query(query=query) - def unsync(self, parameterized: param.Parameterized, parameters: Optional[list[str]] = None) -> None: + def unsync(self, parameterized: param.Parameterized, parameters: list[str] | None = None) -> None: """ Unsyncs the parameters of the Parameterized with the query params in the URL. If no parameters are supplied all diff --git a/panel/io/mime_render.py b/panel/io/mime_render.py index 2cce986733..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 @@ -122,9 +122,9 @@ def _display(*objs, **kwargs): def exec_with_return( code: str, - global_context: dict[str, Any] = None, - stdout: Any = None, - stderr: Any = None + global_context: dict[str, Any] | None = 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 f7a02874b9..013ac6b361 100644 --- a/panel/io/model.py +++ b/panel/io/model.py @@ -5,17 +5,17 @@ import textwrap +from collections.abc import Iterable, Sequence from contextlib import contextmanager -from typing import ( - TYPE_CHECKING, Any, Iterable, Optional, -) +from typing import TYPE_CHECKING, Any import numpy as np from bokeh.core.serialization import Serializer from bokeh.document import Document from bokeh.document.events import ( - ColumnDataChangedEvent, DocumentPatchedEvent, ModelChangedEvent, + ColumnDataChangedEvent, DocumentChangedEvent, DocumentPatchedEvent, + ModelChangedEvent, ) from bokeh.document.json import PatchJson from bokeh.model import DataModel @@ -27,7 +27,6 @@ if TYPE_CHECKING: from bokeh.core.enums import HoldPolicyType - from bokeh.document.events import DocumentChangedEvent from bokeh.protocol.message import Message from pyviz_comms import Comm @@ -46,7 +45,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[DocumentChangedEvent]) -> None: +def monkeypatch_events(events: Sequence[DocumentChangedEvent]) -> None: """ Patch events applies patches to events that are to be dispatched avoiding various issues in Bokeh. @@ -67,7 +66,7 @@ def monkeypatch_events(events: list[DocumentChangedEvent]) -> None: #--------------------------------------------------------------------- def diff( - doc: Document, binary: bool = True, events: Optional[list[DocumentChangedEvent]] = None + doc: Document, binary: bool = True, events: list[DocumentChangedEvent] | None = None ) -> Message[Any] | None: """ Returns a json diff required to update an existing plot with @@ -80,7 +79,7 @@ def diff( patch_events = [event for event in events if isinstance(event, DocumentPatchedEvent)] if not patch_events: - return + return None monkeypatch_events(patch_events) serializer = Serializer(references=doc.models.synced_references, deferred=binary) patch_json = PatchJson(events=serializer.encode(patch_events)) @@ -93,7 +92,7 @@ def diff( msg.add_buffer(buffer) return msg -def remove_root(obj: Model, replace: Document | None = None, skip: set[Model] | None = None) -> None: +def remove_root(obj: Model, replace: Document | None = None, skip: set[Model] | None = None) -> set[Model]: """ Removes the document from any previously displayed bokeh object """ @@ -138,7 +137,7 @@ def patch_cds_msg(model, msg): _DEFAULT_IGNORED_REPR = frozenset(['children', 'text', 'name', 'toolbar', 'renderers', 'below', 'center', 'left', 'right']) -def bokeh_repr(obj: Model, depth: int = 0, ignored: Optional[Iterable[str]] = None) -> str: +def bokeh_repr(obj: Model, depth: int = 0, ignored: Iterable[str] | None = None) -> str: """ Returns a string repr for a bokeh model, useful for recreating panel objects using pure bokeh. diff --git a/panel/io/notebook.py b/panel/io/notebook.py index f0a94575fe..fe5ef15a09 100644 --- a/panel/io/notebook.py +++ b/panel/io/notebook.py @@ -10,11 +10,10 @@ import uuid import warnings +from collections.abc import Iterator from contextlib import contextmanager from functools import partial -from typing import ( - TYPE_CHECKING, Any, Iterator, Literal, Optional, -) +from typing import TYPE_CHECKING, Any, Literal import bokeh import bokeh.embed.notebook @@ -48,6 +47,7 @@ from .state import state if TYPE_CHECKING: + from bokeh.protocol.message import Message from bokeh.server.server import Server from jinja2 import Template @@ -68,7 +68,7 @@ def _jupyter_server_extension_paths() -> list[dict[str, str]]: return [{"module": "panel.io.jupyter_server_extension"}] -def push(doc: Document, comm: Comm, binary: bool = True, msg: any = None) -> None: +def push(doc: Document, comm: Comm, binary: bool = True, msg: Message | None = None) -> None: """ Pushes events stored on the document across the provided comm. """ @@ -89,7 +89,7 @@ def push(doc: Document, comm: Comm, binary: bool = True, msg: any = None) -> Non else: send(comm, msg) -def send(comm: Comm, msg: any): +def send(comm: Comm, msg: Message): """ Sends a bokeh message across a pyviz_comms.Comm. """ @@ -166,7 +166,7 @@ def html_for_render_items(docs_json, render_items, template=None, template_varia return template.render(context) def render_template( - document: 'Document', comm: Optional['Comm'] = None, manager: Optional['CommManager'] = None + document: Document, comm: Comm | None = None, manager: CommManager | None = None ) -> tuple[dict[str, str], dict[str, dict[str, str]]]: ref = document.roots[0].ref['id'] (docs_json, render_items) = standalone_docs_json_and_render_items(document, suppress_callback_warning=True) @@ -184,7 +184,7 @@ def render_template( return ({'text/html': html, EXEC_MIME: ''}, {EXEC_MIME: {'id': ref}}) def render_model( - model: 'Model', comm: Optional['Comm'] = None, resources: str = 'cdn' + model: Model, comm: Comm | None = None, resources: str = 'cdn' ) -> tuple[dict[str, str], dict[str, dict[str, str]]]: if not isinstance(model, Model): raise ValueError("notebook_content expects a single Model instance") @@ -196,7 +196,8 @@ def render_model( # ALERT: Replace with better approach before Bokeh 3.x compatible release dist_url = '/panel-preview/static/extensions/panel/' patch_model_css(model, dist_url=dist_url) - model.document._template_variables['dist_url'] = dist_url + if model.document: + model.document._template_variables['dist_url'] = dist_url (docs_json, [render_item]) = standalone_docs_json_and_render_items([model], suppress_callback_warning=True) div = div_for_render_item(render_item) @@ -235,9 +236,9 @@ def _repr_mimebundle_(include=None, exclude=None): def render_mimebundle( - model: 'Model', doc: 'Document', comm: 'Comm', - manager: Optional['CommManager'] = None, - location: Optional['Location'] = None, + model: Model, doc: Document, comm: Comm, + manager: CommManager | None = None, + location: Location | None = None, resources: str = 'cdn' ) -> tuple[dict[str, str], dict[str, dict[str, str]]]: """ @@ -442,7 +443,7 @@ def load_notebook( publish_display_data(data={LOAD_MIME: JS, 'application/javascript': JS}) -def show_server(panel: Any, notebook_url: str, port: int = 0) -> 'Server': +def show_server(panel: Any, notebook_url: str, port: int = 0) -> Server: """ Displays a bokeh server inline in the notebook. @@ -478,7 +479,7 @@ def show_server(panel: Any, notebook_url: str, port: int = 0) -> 'Server': if callable(notebook_url): url = notebook_url(server.port) else: - url = _server_url(notebook_url, server.port) + url = _server_url(notebook_url, server.port or port) script = server_document(url, resources=None) @@ -492,9 +493,9 @@ def show_server(panel: Any, notebook_url: str, port: int = 0) -> 'Server': def render_embed( panel, 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 = True, states: dict[Widget, list[Any]] = {} -) -> 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 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/profile.py b/panel/io/profile.py index fb39f1d0c6..7dfb488242 100644 --- a/panel/io/profile.py +++ b/panel/io/profile.py @@ -6,11 +6,12 @@ import tempfile import uuid +from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from cProfile import Profile from functools import wraps from typing import ( - TYPE_CHECKING, Callable, Iterator, Literal, ParamSpec, TypeVar, + TYPE_CHECKING, Literal, ParamSpec, TypeVar, ) from ..config import config @@ -18,6 +19,8 @@ from .state import state if TYPE_CHECKING: + from pyinstrument.session import Session + _P = ParamSpec("_P") _R = TypeVar("_R") @@ -194,7 +197,7 @@ def update_memray(*args): @contextmanager -def profile_ctx(engine: ProfilingEngine = 'pyinstrument') -> Iterator[list[Profile | bytes]]: +def profile_ctx(engine: ProfilingEngine = 'pyinstrument') -> Iterator[Sequence[Profile | bytes | Session]]: """ A context manager which profiles the body of the with statement with the supplied profiling engine and returns the profiling object @@ -219,8 +222,8 @@ def profile_ctx(engine: ProfilingEngine = 'pyinstrument') -> Iterator[list[Profi prof = Profiler(async_mode='disabled') prof.start() elif engine == 'snakeviz': - prof = Profile() - prof.enable() + profile = Profile() + profile.enable() elif engine == 'memray': import memray tmp_file = f'{tempfile.gettempdir()}/tmp{uuid.uuid4().hex}' @@ -228,13 +231,13 @@ def profile_ctx(engine: ProfilingEngine = 'pyinstrument') -> Iterator[list[Profi tracker.__enter__() elif engine is None: pass - sessions: list[Profile | bytes] = [] + sessions: Sequence[Profile | bytes | Session] = [] yield sessions if engine == 'pyinstrument': sessions.append(prof.stop()) elif engine == 'snakeviz': - prof.disable() - sessions.append(prof) + profile.disable() + sessions.append(profile) elif engine == 'memray': tracker.__exit__(None, None, None) sessions.append(open(tmp_file, 'rb').read()) @@ -262,7 +265,7 @@ def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R: return func(*args, **kwargs) with profile_ctx(engine) as sessions: ret = func(*args, **kwargs) - state._profiles[(name, engine)] += sessions + state._profiles[(name, engine)] += list(sessions) state.param.trigger('_profiles') return ret return wrapped diff --git a/panel/io/pyodide.py b/panel/io/pyodide.py index 4cbbd4c6ca..b1c52305d9 100644 --- a/panel/io/pyodide.py +++ b/panel/io/pyodide.py @@ -8,7 +8,8 @@ import sys import uuid -from typing import Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any import bokeh import js @@ -40,6 +41,12 @@ resources.RESOURCE_MODE = 'CDN' os.environ['BOKEH_RESOURCES'] = 'cdn' +if TYPE_CHECKING: + from bokeh.core.types import ID + + from ..template.base import TemplateBase + from ..viewable import Viewable + try: from js import document as js_document # noqa try: @@ -106,7 +113,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 +121,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() @@ -162,20 +169,22 @@ def _doc_json(doc: Document, root_els=None) -> tuple[str, str, str]: }) return json.dumps(docs_json), json.dumps(render_items_json), json.dumps(root_ids) -def _model_json(model: Model, target: str) -> tuple[Document, str]: +def _model_json(viewable: Viewable | TemplateBase, target: str) -> tuple[Document, str]: """ Renders a Bokeh Model to JSON representation given a particular DOM target and returns the Document and the serialized JSON string. Arguments --------- - model: bokeh.model.Model + model: Viewable The bokeh model to render. target: str The id of the DOM node to render to. Returns ------- + viewable: The Viewable to render to JSON + The viewable to render document: Document The bokeh Document containing the rendered Bokeh Model. model_json: str @@ -183,7 +192,7 @@ def _model_json(model: Model, target: str) -> tuple[Document, str]: """ doc = Document() doc.hold() - model.server_doc(doc=doc) + viewable.server_doc(doc=doc) model = doc.roots[0] docs_json, _ = standalone_docs_json_and_render_items( [model], suppress_callback_warning=True @@ -394,7 +403,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 +446,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 +529,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 +584,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/reload.py b/panel/io/reload.py index 8aee616919..0acf2172fd 100644 --- a/panel/io/reload.py +++ b/panel/io/reload.py @@ -2,6 +2,7 @@ import fnmatch import logging import os +import pathlib import sys import types import warnings @@ -13,12 +14,15 @@ try: from watchfiles import awatch except Exception: - async def awatch(*files, stop_event=None): + async def awatch( # type: ignore + *paths: pathlib.Path | str, + stop_event: asyncio.Event | None = None + ): stop_event = stop_event or asyncio.Event() - modify_times = {} + modify_times: dict[str | os.PathLike, int | float] = {} while not stop_event.is_set(): changes = set() - for path in files: + for path in paths: change = _check_file(path, modify_times) if change: changes.add((change, path)) @@ -31,9 +35,9 @@ async def awatch(*files, stop_event=None): _reload_logger = logging.getLogger('panel.io.reload') -_watched_files = set() -_modules = set() -_local_modules = set() +_watched_files: set[str] = set() +_modules: set[str] = set() +_local_modules: set[str] = set() # List of paths to ignore DEFAULT_FOLDER_DENYLIST = [ @@ -56,7 +60,8 @@ async def awatch(*files, stop_event=None): 'bokeh_app', 'geoviews.models.', 'panel.', - 'torch.' + 'torch.', + 'defusedxml' ] def in_denylist(filepath): @@ -242,7 +247,7 @@ def reload_session(event, loc=loc): loc.reload = True doc.on_event('document_ready', reload_session) -def _check_file(path, modify_times): +def _check_file(path: str | os.PathLike, modify_times: dict[str | os.PathLike, int | float]): """ Checks if a file was modified or deleted and then returns a code, modeled after watchfiles, indicating the type of change: diff --git a/panel/io/resources.py b/panel/io/resources.py index fdf5ac13d2..99df9deafe 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 @@ -42,11 +44,20 @@ if TYPE_CHECKING: from bokeh.resources import Urls - class ResourcesType(TypedDict): + class TarballType(TypedDict, total=False): + tar: str + src: str + dest: str + exclude: list[str] + + class ResourcesType(TypedDict, total=False): css: dict[str, str] - js: dict[str, str] + font: dict[str, str] + js: dict[str, str] js_modules: dict[str, str] raw_css: list[str] + tarball: dict[str, TarballType] + bundle: bool logger = logging.getLogger(__name__) @@ -145,7 +156,7 @@ def parse_template(*args, **kwargs): 'bootstrap5': f'{CDN_DIST}bundled/bootstrap5/js/bootstrap.bundle.min.js' } -extension_dirs['panel'] = str(DIST_DIR) +extension_dirs['panel'] = DIST_DIR bokeh.embed.wrappers._ONLOAD = """\ (function() { @@ -222,11 +233,13 @@ def resolve_custom_path( path: pathlib.Path | None """ if not path: - return + return None if not isinstance(obj, type): obj = type(obj) try: mod = importlib.import_module(obj.__module__) + if mod.__file__ is None: + return None module_path = Path(mod.__file__).parent assert module_path.exists() except Exception: @@ -244,7 +257,7 @@ def resolve_custom_path( abs_path = abs_path.resolve() if not relative: return abs_path - return os.path.relpath(abs_path, module_path) + return pathlib.Path(os.path.relpath(abs_path, module_path)) def component_resource_path(component, attr, path): """ @@ -469,7 +482,7 @@ class ResourceComponent: that have to be resolved. """ - _resources = { + _resources: ClassVar[ResourcesType] = { 'css': {}, 'font': {}, 'js': {}, @@ -478,7 +491,12 @@ class ResourceComponent: } @classmethod - def _resolve_resource(cls, resource_type: str, resource: str, cdn: bool = False): + def _resolve_resource( + cls, + resource_type: str, + resource: str, + cdn: bool = False + ) -> str: dist_path = get_dist_path(cdn=cdn) if resource.startswith(CDN_DIST): resource_path = resource.replace(f'{CDN_DIST}bundled/', '') @@ -508,6 +526,9 @@ def _resolve_resource(cls, resource_type: str, resource: str, cdn: bool = False) return component_resource_path( cls, f'_resources/{resource_type}', resource ) + raise FileNotFoundError( + f'Could not resolve resource {resource!r}' + ) def resolve_resources( self, @@ -532,7 +553,7 @@ def resolve_resources( Dictionary containing JS and CSS resources. """ cls = type(self) - resources = {} + resources: ResourcesType = {} for rt, res in self._resources.items(): if not isinstance(res, dict): continue @@ -543,9 +564,9 @@ def resolve_resources( for name, url in res.items() } if rt in resources: - resources[rt] = dict(resources[rt], **res) + resources[rt] = dict(resources[rt], **res) # type: ignore else: - resources[rt] = res + resources[rt] = res # type: ignore resource_types: ResourcesType = { 'js': {}, @@ -556,20 +577,20 @@ def resolve_resources( cdn = use_cdn() if cdn == 'auto' else cdn for resource_type in resource_types: - if resource_type not in resources or resource_type == 'raw_css': + if resource_type not in resources or resource_type == 'raw_css': # type: ignore continue - resource_files = resource_types[resource_type] - for rname, resource in resources[resource_type].items(): + resource_files = resource_types[resource_type] # type: ignore + for rname, resource in resources[resource_type].items(): # type: ignore resolved_resource = self._resolve_resource( resource_type, resource, cdn=cdn ) if resolved_resource: - resource_files[rname] = resolved_resource + resource_files[rname] = resolved_resource # type: ignore version_suffix = f'?v={JS_VERSION}' dist_path = get_dist_path(cdn=cdn) for resource_type, extra_resources in (extras or {}).items(): - resource_files = resource_types[resource_type] + resource_files = resource_types[resource_type] # type: ignore for name, res in extra_resources.items(): if not cdn: res = res.replace(CDN_DIST, dist_path) @@ -717,7 +738,7 @@ def dist_dir(self): def css_files(self): from ..config import config - files = super(Resources, self).css_files + files = super().css_files self.extra_resources(files, '__css__') css_files = self.adjust_paths([ css for css in files if self.mode != 'inline' or not is_cdn_url(css) @@ -733,7 +754,7 @@ def css_files(self): @property def css_raw(self): from ..config import config - raw = super(Resources, self).css_raw + raw = super().css_raw # Inline local dist resources css_files = self._collect_external_resources("__css__") @@ -768,7 +789,7 @@ def js_files(self): # Gather JS files with set_resource_mode(self.mode): - files = super(Resources, self).js_files + files = super().js_files self.extra_resources(files, '__javascript__') files += [js for js in config.js_files.values()] if config.design: @@ -840,7 +861,7 @@ def js_module_exports(self): @property def js_raw(self): - raw_js = super(Resources, self).js_raw + raw_js = super().js_raw if not self.mode == 'inline': return raw_js diff --git a/panel/io/save.py b/panel/io/save.py index 79b81f6f46..494c0af429 100644 --- a/panel/io/save.py +++ b/panel/io/save.py @@ -6,9 +6,8 @@ import io import os -from typing import ( - IO, TYPE_CHECKING, Any, Iterable, Optional, -) +from collections.abc import Iterable +from typing import IO, TYPE_CHECKING, Any import bokeh @@ -19,6 +18,7 @@ ) from bokeh.io.export import get_screenshot_as_png from bokeh.model import Model +from bokeh.models import UIElement from bokeh.resources import CDN, INLINE, Resources as BkResources from pyviz_comms import Comm @@ -59,8 +59,12 @@ bokeh.io.export._WAIT_SCRIPT = _WAIT_SCRIPT def save_png( - model: Model, filename: str, resources=CDN, template=None, - template_variables=None, timeout: int = 5 + model: UIElement | Document, + filename: str | os.PathLike | IO, + resources: BkResources = CDN, + template=None, + template_variables: dict[str, Any] | None = None, + timeout: int = 5 ) -> None: """ Saves a bokeh model to png @@ -104,7 +108,7 @@ def save_png( """ try: - def get_layout_html(obj, resources, width, height, **kwargs): + def get_layout_html(obj: UIElement | Document, resources: Resources, width: int | None, height: int | None, **kwargs): resources = Resources.from_bokeh(resources) return file_html( obj, resources, title="", template=template, @@ -112,7 +116,7 @@ def get_layout_html(obj, resources, width, height, **kwargs): _always_new=True ) old_layout_fn = bokeh.io.export.get_layout_html - bokeh.io.export.get_layout_html = get_layout_html + bokeh.io.export.get_layout_html = get_layout_html # type: ignore img = get_screenshot_as_png(model, driver=webdriver, timeout=timeout, resources=resources) if img.width == 0 or img.height == 0: @@ -140,8 +144,8 @@ def _title_from_models(models: Iterable[Model], title: str) -> str: return DEFAULT_TITLE def file_html( - models: Model | Document | list[Model], resources: Resources | None, - title: Optional[str] = None, template: Template | str = BASE_TEMPLATE, + models: Model | Document | list[Model], resources: BkResources | None, + title: str | None = None, template: Template | str = BASE_TEMPLATE, template_variables: dict[str, Any] = {}, theme: ThemeLike = None, _always_new: bool = False ): @@ -159,7 +163,7 @@ def file_html( (docs_json, render_items) = standalone_docs_json_and_render_items( models_seq, suppress_callback_warning=True ) - title = _title_from_models(models_seq, title) + title = _title_from_models(models_seq, title or 'Panel Application') bundle = bundle_resources(models_seq, resources) return html_page_for_render_items( bundle, docs_json, render_items, title=title, template=template, @@ -171,11 +175,11 @@ def file_html( #--------------------------------------------------------------------- def save( - panel: Viewable, filename: str | os.PathLike | IO, title: Optional[str]=None, + panel: Viewable | Document, filename: str | os.PathLike | IO, title: str | None = None, resources: BkResources | None = None, template: Template | str | None = None, - template_variables: dict[str, Any] = None, embed: bool = False, + template_variables: dict[str, Any] | None = None, embed: bool = False, max_states: int = 1000, max_opts: int = 3, embed_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 = True, embed_states={}, as_png=None, **kwargs ) -> None: @@ -252,7 +256,7 @@ def save( comm = Comm() with config.set(embed=embed): if isinstance(panel, Document): - model = panel + model: Document | Model = panel elif isinstance(panel, BaseTemplate): with set_resource_mode(mode): panel._init_doc(doc, title=title) @@ -268,6 +272,9 @@ def save( else: add_to_doc(model, doc, True) + if isinstance(model, Model) and not isinstance(model, UIElement): + raise ValueError("Cannot render non-UI components.") + if as_png: return save_png( model, resources=resources, filename=filename, template=template, diff --git a/panel/io/server.py b/panel/io/server.py index 61bb6c1cb7..238d99120d 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -14,10 +14,11 @@ import sys import uuid +from collections.abc import Callable, Mapping from functools import partial, wraps from html import escape from typing import ( - TYPE_CHECKING, Any, Callable, Mapping, Optional, + TYPE_CHECKING, Any, Literal, TypedDict, ) from urllib.parse import urlparse @@ -76,16 +77,21 @@ 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 from .application import TViewableFuncOrPath from .location import Location + class TokenPayload(TypedDict): + headers: dict[str, Any] + cookies: dict[str, Any] + arguments: dict[str, Any] + #--------------------------------------------------------------------- # Private API @@ -126,19 +132,19 @@ def async_execute(func: Callable[..., None]) -> None: unlock = not getattr(func, 'lock', False) curdoc = state.curdoc @wraps(func) - async def wrapper(*args, **kw): + async def wrapped(*args, **kw): with set_curdoc(curdoc): try: return await func(*args, **kw) except Exception as e: state._handle_exception(e) if unlock: - wrapper.nolock = True # type: ignore - state.curdoc.add_next_tick_callback(wrapper) + wrapped.nolock = True # type: ignore + state.curdoc.add_next_tick_callback(wrapped) 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'] @@ -151,12 +157,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') @@ -221,20 +229,22 @@ 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( - session: 'ServerSession', - resources: 'Resources', + session: ServerSession, + resources: Resources, title: str, token: str | None = None, template: str | Template = BASE_TEMPLATE, - template_variables: Optional[dict[str, Any]] = None, + template_variables: dict[str, Any] | None = None, ) -> str: # ALERT: Replace with better approach before Bokeh 3.x compatible release @@ -322,7 +332,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: """ @@ -344,8 +354,8 @@ def get_login_url(self): class DocHandler(LoginUrlMixin, BkDocHandler): - @authenticated - async def get_session(self): + @authenticated # type: ignore + async def get_session(self) -> ServerSession: from ..config import config path = self.request.path session = None @@ -353,7 +363,7 @@ async def get_session(self): 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'))) @@ -363,7 +373,7 @@ async def get_session(self): session.block_expiration() return session - def _generate_token_payload(self): + def _generate_token_payload(self) -> TokenPayload: app = self.application if app.include_headers is None: excluded_headers = (app.exclude_headers or []) @@ -388,12 +398,13 @@ def _generate_token_payload(self): del headers['Cookie'] arguments = {} if self.request.arguments is None else self.request.arguments - payload = {'headers': headers, 'cookies': cookies, 'arguments': arguments} - payload.update(self.application_context.application.process_request(self.request)) + payload: TokenPayload = {'headers': headers, 'cookies': cookies, 'arguments': arguments} + payload.update(self.application_context.application.process_request(self.request)) # type: ignore return payload - def _authorize(self, session=False): + def _authorize(self, session: bool = False) -> tuple[bool, str | None]: """ + Determine if user is authorized to access this application. """ auth_cb = config.authorize_callback # If inside a session ensure the authorize callback is not global @@ -401,6 +412,7 @@ def _authorize(self, session=False): 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: @@ -411,7 +423,7 @@ def _authorize(self, session=False): '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): @@ -429,7 +441,7 @@ def _authorize(self, session=False): logger.warning(auth_error) return authorized, auth_error - def _render_auth_error(self, auth_error): + def _render_auth_error(self, auth_error: str) -> str: if config.auth_template: with open(config.auth_template) as f: template = _env.from_string(f.read()) @@ -524,7 +536,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 @@ -550,7 +562,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): @@ -601,7 +613,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 | Literal['root'] = 'root', default_filename: str | None = None): self.root = path self.default_filename = default_filename @@ -679,14 +691,14 @@ 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, - loop: Optional[IOLoop] = None, + address: str | None = None, + websocket_origin: str | list[str] | None = None, + loop: IOLoop | None = None, show: bool = True, start: bool = True, - title: Optional[str] = None, + title: str | None = None, verbose: bool = True, location: bool = True, threaded: bool = False, @@ -804,11 +816,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, @@ -816,24 +828,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 @@ -1016,7 +1028,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, @@ -1037,13 +1049,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 a729100d64..2101aa01ea 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -14,13 +14,14 @@ import time from collections import Counter, defaultdict -from collections.abc import Iterator +from collections.abc import ( + Callable, Coroutine, Hashable, Iterator, Iterator as TIterator, +) from contextlib import contextmanager, suppress from contextvars import ContextVar from functools import partial, wraps from typing import ( - TYPE_CHECKING, Any, Awaitable, Callable, ClassVar, Coroutine, - Iterator as TIterator, Literal, Optional, TypeVar, Union, + TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias, TypeVar, ) from urllib.parse import urljoin from weakref import WeakKeyDictionary @@ -30,6 +31,7 @@ from bokeh.document import Document from bokeh.document.locking import UnlockedDocumentProxy from bokeh.io import curdoc as _curdoc +from param.parameterized import Event, Parameterized from pyviz_comms import CommManager as _CommManager from ..util import decode_token, parse_timedelta @@ -40,10 +42,11 @@ if TYPE_CHECKING: from concurrent.futures import Future + from bokeh.application.application import SessionContext from bokeh.model import Model from bokeh.models import ImportedStyleSheet from bokeh.server.contexts import BokehSessionContext - from bokeh.server.server import Server + from bokeh.server.session import ServerSession from IPython.display import DisplayHandle from pyviz_comms import Comm from tornado.ioloop import IOLoop @@ -51,26 +54,31 @@ 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 from .location import Location from .notifications import NotificationArea from .server import StoppableThread T = TypeVar("T") + K = TypeVar('K', bound=Hashable) @contextmanager -def set_curdoc(doc: Document): - token = state._curdoc.set(doc) +def set_curdoc(doc: Document | None): + if doc: + token = state._curdoc.set(doc) try: yield finally: - # If _curdoc has been reset it will raise a ValueError - with suppress(ValueError): - state._curdoc.reset(token) + if doc: + # If _curdoc has been reset it will raise a ValueError + with suppress(ValueError): + state._curdoc.reset(token) -def curdoc_locked() -> Document: +def curdoc_locked() -> Document | None: try: doc = _curdoc() except RuntimeError: @@ -81,7 +89,7 @@ def curdoc_locked() -> Document: class _Undefined: pass -Tat = Union[dt.datetime, Callable[[dt.datetime], dt.datetime], TIterator[dt.datetime]] +Tat: TypeAlias = dt.datetime | Callable[[dt.datetime], dt.datetime] | TIterator[dt.datetime] class _state(param.Parameterized): """ @@ -123,10 +131,10 @@ class _state(param.Parameterized): this will instead reflect an absolute path.""") # Holds temporary curdoc overrides per thread - _curdoc = ContextVar('curdoc', default=None) + _curdoc: ContextVar[Document | None] = ContextVar('curdoc', default=None) # Whether to hold comm events - _hold: ClassVar[bool] = False + _hold: bool = False # Used to ensure that events are not scheduled from the wrong thread _thread_id_: ClassVar[WeakKeyDictionary[Document, int]] = WeakKeyDictionary() @@ -136,9 +144,9 @@ class _state(param.Parameterized): _admin_context = None # Jupyter communication - _comm_manager: ClassVar[type[_CommManager]] = _CommManager - _jupyter_kernel_context: ClassVar[BokehSessionContext | None] = None - _kernels = {} + _comm_manager: type[_CommManager] = _CommManager + _jupyter_kernel_context: BokehSessionContext | None = None + _kernels: ClassVar[dict[str, tuple[Any, str, str, bool]]] = {} _ipykernels: ClassVar[WeakKeyDictionary[Document, Any]] = WeakKeyDictionary() # Locations @@ -164,39 +172,39 @@ 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[Any, TViewableFuncOrPath | dict[str, TViewableFuncOrPath], list[Document]]]] = {} _threads: ClassVar[dict[str, StoppableThread]] = {} _server_config: ClassVar[WeakKeyDictionary[Any, dict[str, Any]]] = WeakKeyDictionary() # Jupyter display handles - _handles: ClassVar[dict[str, [DisplayHandle, list[str]]]] = {} + _handles: ClassVar[dict[str, tuple[DisplayHandle, list[str]]]] = {} # Stacks for hashing - _stacks = WeakKeyDictionary() + _stacks: WeakKeyDictionary[threading.Thread, _Stack] = WeakKeyDictionary() # Dictionary of callbacks to be triggered on app load - _onload: ClassVar[dict[Document, Callable[[], None]]] = WeakKeyDictionary() - _on_session_created: ClassVar[list[Callable[[BokehSessionContext], None]]] = [] - _on_session_created_internal: ClassVar[list[Callable[[BokehSessionContext], None]]] = [] - _on_session_destroyed: ClassVar[list[Callable[[BokehSessionContext], None]]] = [] + _onload: ClassVar[WeakKeyDictionary[Document, list[tuple[Callable[[], None | Coroutine[Any, Any, None]], bool]]]] = WeakKeyDictionary() + _on_session_created: ClassVar[list[Callable[[SessionContext], None]]] = [] + _on_session_created_internal: ClassVar[list[Callable[[SessionContext], None]]] = [] + _on_session_destroyed: ClassVar[list[Callable[[SessionContext], None]]] = [] _loaded: ClassVar[WeakKeyDictionary[Document, bool]] = WeakKeyDictionary() # Module that was run during setup _setup_module = None # Scheduled callbacks - _scheduled: ClassVar[dict[str, tuple[Iterator[int], Callable[[], None]]]] = {} + _scheduled: ClassVar[dict[str, tuple[TIterator[int] | None, Callable[[], None]]]] = {} _periodic: ClassVar[WeakKeyDictionary[Document, list[PeriodicCallback]]] = WeakKeyDictionary() # Indicators listening to the busy state _indicators: ClassVar[list[BooleanIndicator]] = [] # Profilers - _launching = [] + _launching: ClassVar[set[Document]] = set() _profiles = param.Dict(default=defaultdict(list)) # Endpoints - _rest_endpoints = {} + _rest_endpoints: ClassVar[dict[str, tuple[list[Parameterized], list[str], Callable[[Event], Any]]]] = {} # Style cache _stylesheets: ClassVar[WeakKeyDictionary[Document, dict[str, ImportedStyleSheet]]] = WeakKeyDictionary() @@ -205,40 +213,40 @@ class _state(param.Parameterized): _extensions_: ClassVar[WeakKeyDictionary[Document, list[str]]] = WeakKeyDictionary() # Locks - _cache_locks: ClassVar[dict[str, threading.Lock]] = {'main': threading.Lock()} + _cache_locks: ClassVar[dict[str | tuple[Any, ...], threading.Lock]] = {'main': threading.Lock()} # Sessions - _sessions = {} - _session_key_funcs = {} + _sessions: ClassVar[dict[Hashable, ServerSession]] = {} + _session_key_funcs: ClassVar[dict[str, Callable[[Any], Any]]] = {} # Layout editor - _cell_outputs = defaultdict(list) - _cell_layouts = defaultdict(dict) + _cell_outputs: ClassVar[defaultdict[Hashable, list[Any]]] = defaultdict(list) + _cell_layouts: ClassVar[defaultdict[Hashable, dict[str, dict]]] = defaultdict(dict) _session_outputs: ClassVar[WeakKeyDictionary[Document, dict[str, Any]]] = WeakKeyDictionary() # Override user info - _oauth_user_overrides = {} - _active_users = Counter() + _oauth_user_overrides: ClassVar[dict[str, dict[str, Any]]] = {} + _active_users: ClassVar[Counter[str]] = Counter() # Paths _rel_paths: ClassVar[WeakKeyDictionary[Document, str]] = WeakKeyDictionary() _base_urls: ClassVar[WeakKeyDictionary[Document, str]] = WeakKeyDictionary() # Watchers - _watch_events: list[asyncio.Event] = [] + _watch_events: ClassVar[list[asyncio.Event]] = [] def __repr__(self) -> str: server_info = [] for server, panel, _docs in self._servers.values(): server_info.append( - "{}:{:d} - {!r}".format(server.address or "localhost", server.port, panel) + "{}:{:d} - {!r}".format(server.address or "localhost", server.port or 0, panel) ) if not server_info: return "state(servers=[])" return "state(servers=[\n {}\n])".format(",\n ".join(server_info)) @property - def _ioloop(self) -> 'IOLoop': + def _ioloop(self) -> IOLoop | asyncio.AbstractEventLoop: if state._is_pyodide: return asyncio.get_running_loop() else: @@ -253,7 +261,7 @@ def _extensions(self): return self._extensions_[doc] @property - def _current_thread(self) -> str | None: + def _current_thread(self) -> int | None: return threading.get_ident() @property @@ -287,7 +295,7 @@ def _unblocked(self, doc: Document) -> bool: 2. We are on the same thread that the Document was created on. 3. The application has fully loaded and the Websocket is open. """ - return ( + return bool( doc is self.curdoc and self._thread_id in (self._current_thread, None) and (not (doc and doc.session_context and doc.session_context.session) or self._loaded.get(doc)) @@ -400,9 +408,11 @@ def _schedule_on_load(self, doc: Document, event) -> None: else: self._on_load(doc) - def _on_load(self, doc: Optional[Document] = None) -> None: + def _on_load(self, doc: Document | None = None) -> None: doc = doc or self.curdoc - if doc not in self._onload: + if not doc: + return + elif doc not in self._onload: self._loaded[doc] = True return @@ -420,18 +430,23 @@ def _on_load(self, doc: Optional[Document] = None) -> None: while doc in self._onload: for cb, threaded in self._onload.pop(doc): self.execute(cb, schedule='thread' if threaded else False) - path = doc.session_context.request.path - self._profiles[(path+':on_load', config.profiler)] += sessions - self.param.trigger('_profiles') + if doc.session_context: + path = doc.session_context.request.path + self._profiles[(path+':on_load', config.profiler)] += sessions + self.param.trigger('_profiles') self._loaded[doc] = True async def _scheduled_cb(self, name: str, threaded: bool = False) -> None: if name not in self._scheduled: return diter, cb = self._scheduled[name] - try: - at = next(diter) - except Exception: + if diter: + try: + at = next(diter) + except Exception: + at = None + del self._scheduled[name] + else: at = None del self._scheduled[name] if at is not None: @@ -452,7 +467,7 @@ def wrapper(*args, **kw): self._handle_exception(e) return wrapper - def _handle_future_exception(self, future: Future, doc: Document = None) -> None: + def _handle_future_exception(self, future: Future, doc: Document | None = None) -> None: exception = future.exception() if exception is None: return @@ -460,7 +475,7 @@ def _handle_future_exception(self, future: Future, doc: Document = None) -> None with set_curdoc(doc): self._handle_exception(exception) - def _handle_exception(self, exception) -> None: + def _handle_exception(self, exception: BaseException) -> None: from ..config import config if config.exception_handler: config.exception_handler(exception) @@ -469,7 +484,7 @@ def _handle_exception(self, exception) -> None: else: self.log(f'Exception of unknown type raised: {exception}', level='error') - def _register_session_destroyed(self, session_context: BokehSessionContext): + def _register_session_destroyed(self, session_context: SessionContext): for cb in self._on_session_destroyed: session_context._document.on_session_destroyed(cb) @@ -477,7 +492,7 @@ def _register_session_destroyed(self, session_context: BokehSessionContext): # Public Methods #---------------------------------------------------------------- - def as_cached(self, key: str, fn: Callable[[], T], ttl: int = None, **kwargs) -> T: + def as_cached(self, key: str, fn: Callable[[], T], ttl: int | None = None, **kwargs) -> T: """ Caches the return value of a function globally across user sessions, memoizing on the given key and supplied keyword arguments. @@ -513,30 +528,30 @@ def as_cached(self, key: str, fn: Callable[[], T], ttl: int = None, **kwargs) -> Returns the value returned by the cache or the value in the cache. """ - key = (key,)+tuple((k, v) for k, v in sorted(kwargs.items())) + cache_key = (key,)+tuple((k, v) for k, v in sorted(kwargs.items())) new_expiry = time.monotonic() + ttl if ttl else None with self._cache_locks['main']: - if key in self._cache_locks: - lock = self._cache_locks[key] + if cache_key in self._cache_locks: + lock = self._cache_locks[cache_key] else: - self._cache_locks[key] = lock = threading.Lock() + self._cache_locks[cache_key] = lock = threading.Lock() try: with lock: - if key in self.cache: - ret, expiry = self.cache.get(key) + if cache_key in self.cache: + ret, expiry = self.cache.get(cache_key) else: ret, expiry = _Undefined, None if ret is _Undefined or (expiry is not None and expiry < time.monotonic()): - ret, _ = self.cache[key] = (fn(**kwargs), new_expiry) + ret, _ = self.cache[cache_key] = (fn(**kwargs), new_expiry) finally: - if not lock.locked() and key in self._cache_locks: - del self._cache_locks[key] + if not lock.locked() and cache_key in self._cache_locks: + del self._cache_locks[cache_key] return ret def add_periodic_callback( self, callback: Callable[[], None] | Coroutine[Any, Any, None], - period: int=500, count: Optional[int] = None, timeout: int = None, - start: bool=True + period: int = 500, count: int | None = None, timeout: int | None = None, + start: bool = True ) -> PeriodicCallback: """ Schedules a periodic callback to be run at an interval set by @@ -613,7 +628,7 @@ def _execute_on_thread(self, doc, callback): def execute( self, - callback: Callable[[], None], + callback: Callable[[], None | Coroutine[Any, Any, None]], schedule: bool | Literal['auto', 'thread'] = 'auto' ) -> None: """ @@ -693,13 +708,13 @@ def log(self, msg: str, level: str = 'info') -> None: level: str Log level as a string, i.e. 'debug', 'info', 'warning' or 'error'. """ - args = () + args: tuple[()] | tuple[int] = () if self.curdoc: args = (id(self.curdoc),) msg = LOG_USER_MSG.format(msg=msg) getattr(_state_logger, level.lower())(msg, *args) - def onload(self, callback: Callable[[], None | Awaitable[None]] | Coroutine[Any, Any, None], threaded: bool = False): + def onload(self, callback: Callable[[], None | Coroutine[Any, Any, None]], threaded: bool = False): """ Callback that is triggered when a session has been served. @@ -715,12 +730,12 @@ def onload(self, callback: Callable[[], None | Awaitable[None]] | Coroutine[Any, self.execute(callback, schedule='threaded') else: self.execute(callback, schedule=False) - return - elif self.curdoc not in self._onload: - self._onload[self.curdoc] = [] - self._onload[self.curdoc].append((callback, threaded)) + elif self.curdoc in self._onload: + self._onload[self.curdoc].append((callback, threaded)) + else: + self._onload[self.curdoc] = [(callback, threaded)] - def on_session_created(self, callback: Callable[[BokehSessionContext], None]) -> None: + def on_session_created(self, callback: Callable[[SessionContext], None]) -> None: """ Callback that is triggered when a session is created. """ @@ -733,7 +748,7 @@ def on_session_created(self, callback: Callable[[BokehSessionContext], None]) -> ) self._on_session_created.append(callback) - def on_session_destroyed(self, callback: Callable[[BokehSessionContext], None]) -> None: + def on_session_destroyed(self, callback: Callable[[SessionContext], None]) -> None: """ Callback that is triggered when a session is destroyed. """ @@ -747,7 +762,7 @@ def on_session_destroyed(self, callback: Callable[[BokehSessionContext], None]) def publish( self, endpoint: str, parameterized: param.Parameterized, - parameters: Optional[list[str]] = None + parameters: list[str] | None = None ) -> None: """ Publish parameters on a Parameterized object as a REST API. @@ -804,8 +819,12 @@ def reset(self): self._periodic.clear() def schedule_task( - self, name: str, callback: Callable[[], None], at: Tat =None, - period: str | dt.timedelta = None, cron: Optional[str] = None, + self, + name: str, + callback: Callable[[], None], + at: Tat | None = None, + period: str | dt.timedelta | None = None, + cron: str | None = None, threaded : bool = False ) -> None: """ @@ -899,6 +918,11 @@ def dgen(): yield new_time.timestamp() new_time += period diter = dgen() + elif isinstance(at, Iterator) or callable(at): + raise ValueError( + "When scheduling a task with croniter the at value must " + "be a concrete datetime value." + ) else: from croniter import croniter base = dt.datetime.now() if at is None else at @@ -956,7 +980,7 @@ def access_token(self) -> str | None: return self._oauth_user_overrides[self.user]['access_token'] access_token = self._decode_cookie('access_token') if not access_token: - return + return None try: decoded_token = decode_token(access_token) except Exception: @@ -970,8 +994,8 @@ def app_url(self) -> str | None: """ Returns the URL of the app that is currently being executed. """ - if not self.curdoc: - return + if not (self.curdoc and self.curdoc.session_context): + return None app_url = self.curdoc.session_context.server_context.application_context.url app_url = app_url[1:] if app_url.startswith('/') else app_url return urljoin(self.base_url, app_url) @@ -980,14 +1004,15 @@ def app_url(self) -> str | None: def browser_info(self) -> BrowserInfo | None: from ..config import config from .browser import BrowserInfo - if config.browser_info and self.curdoc and self.curdoc.session_context and self.curdoc not in self._browsers: + if (config.browser_info and self.curdoc and self.curdoc.session_context and + self.curdoc not in self._browsers): browser = self._browsers[self.curdoc] = BrowserInfo() elif self.curdoc is None: if self._browser is None and config.browser_info: - self._browser = BrowserInfo() - browser = self._browser + _state._browser = BrowserInfo() + browser = self._browser # type: ignore else: - browser = self._browsers.get(self.curdoc) if self.curdoc else None + browser = self._browsers.get(self.curdoc) if self.curdoc else None # type: ignore return browser @property @@ -1005,6 +1030,7 @@ def curdoc(self) -> Document | None: return doc except Exception: return None + return None @curdoc.setter def curdoc(self, doc: Document) -> None: @@ -1050,7 +1076,9 @@ def rel_path(self) -> str | None: @rel_path.setter def rel_path(self, value: str | None): - if self.curdoc: + if value is None: + return + elif self.curdoc: self._rel_paths[self.curdoc] = value else: self._rel_path = value @@ -1119,7 +1147,7 @@ def refresh_token(self) -> str | None: return self._oauth_user_overrides[self.user]['refresh_token'] refresh_token = self._decode_cookie('refresh_token') if not refresh_token: - return + return None try: decoded_token = decode_token(refresh_token) except ValueError: @@ -1149,7 +1177,7 @@ def session_args(self) -> dict[str, list[bytes]]: @property def template(self) -> BaseTemplate | None: from ..config import config - if self.curdoc in self._templates: + if self.curdoc and self.curdoc in self._templates: return self._templates[self.curdoc] elif self.curdoc is None and self._template: return self._template @@ -1157,7 +1185,7 @@ def template(self) -> BaseTemplate | None: if not config.design: config.design = template.design if self.curdoc is None: - self._template = template + _state._template = template else: self._templates[self.curdoc] = template return template @@ -1177,7 +1205,8 @@ def user(self) -> str | None: user = self.cookies.get('user') if user is None or config.cookie_secret is None: return None - return decode_signed_value(config.cookie_secret, 'user', user).decode('utf-8') + decoded = decode_signed_value(config.cookie_secret, 'user', user) + return None if decoded is None else decoded.decode('utf-8') @property def user_info(self) -> dict[str, Any] | None: diff --git a/panel/io/threads.py b/panel/io/threads.py index 610ba94216..2543d304de 100644 --- a/panel/io/threads.py +++ b/panel/io/threads.py @@ -27,7 +27,7 @@ def run(self) -> None: try: bokeh_server = target(*args, **kwargs) finally: - if hasattr(bokeh_server, 'stop'): + if bokeh_server is not None and hasattr(bokeh_server, 'stop'): # Handle tornado server try: bokeh_server.stop() diff --git a/panel/layout/accordion.py b/panel/layout/accordion.py index b3f085cee5..f48e193a6d 100644 --- a/panel/layout/accordion.py +++ b/panel/layout/accordion.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, Callable, ClassVar, Mapping, -) +from collections.abc import Callable, Mapping +from typing import TYPE_CHECKING, ClassVar import param diff --git a/panel/layout/base.py b/panel/layout/base.py index 70160e3fba..9fbea625d0 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -5,10 +5,10 @@ from __future__ import annotations from collections import defaultdict, namedtuple -from typing import ( - TYPE_CHECKING, Any, ClassVar, Generator, Iterable, Iterator, Mapping, - Optional, +from collections.abc import ( + Generator, Iterable, Iterator, Mapping, ) +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -89,7 +89,7 @@ def __repr__(self, depth: int = 0, max_depth: int = 10) -> str: def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: msg = dict(msg) inverse = {v: k for k, v in self._property_mapping.items() if v is not None} @@ -140,7 +140,7 @@ def _update_model( def _get_objects( self, model: Model, old_objects: list[Viewable], doc: Document, - root: Model, comm: Optional[Comm] = None + root: Model, comm: Comm | None = None ): """ Returns new child models for the layout while reusing unchanged @@ -171,8 +171,8 @@ def _get_objects( return new_models, old_models def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: if self._bokeh_model is None: raise ValueError(f'{type(self).__name__} did not define a _bokeh_model.') @@ -310,7 +310,7 @@ def _compute_sizing_mode(self, children, props): #---------------------------------------------------------------- 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: root = super().get_root(doc, comm, preprocess) @@ -370,18 +370,18 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[Viewable]: yield from self.objects - def __iadd__(self, other: Iterable[Any]) -> 'ListLike': + def __iadd__(self, other: Iterable[Any]) -> ListLike: self.extend(other) return self - def __add__(self, other: Iterable[Any]) -> 'ListLike': + def __add__(self, other: Iterable[Any]) -> ListLike: if isinstance(other, ListLike): other = other.objects else: other = list(other) return self.clone(*(self.objects+other)) - def __radd__(self, other: Iterable[Any]) -> 'ListLike': + def __radd__(self, other: Iterable[Any]) -> ListLike: if isinstance(other, ListLike): other = other.objects else: @@ -426,7 +426,7 @@ def __setitem__(self, index: int | slice, panes: Iterable[Any]) -> None: self.objects = new_objects - def clone(self, *objects: Any, **params: Any) -> 'ListLike': + def clone(self, *objects: Any, **params: Any) -> ListLike: """ Makes a copy of the layout sharing the same parameters. @@ -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 @@ -619,11 +619,11 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[Viewable]: yield from self.objects - def __iadd__(self, other: Iterable[Any]) -> 'NamedListLike': + def __iadd__(self, other: Iterable[Any]) -> NamedListLike: self.extend(other) return self - def __add__(self, other: Iterable[Any]) -> 'NamedListLike': + def __add__(self, other: Iterable[Any]) -> NamedListLike: if isinstance(other, NamedListLike): added = list(zip(other._names, other.objects)) elif isinstance(other, ListLike): @@ -633,7 +633,7 @@ def __add__(self, other: Iterable[Any]) -> 'NamedListLike': objects = list(zip(self._names, self.objects)) return self.clone(*(objects+added)) - def __radd__(self, other: Iterable[Any]) -> 'NamedListLike': + def __radd__(self, other: Iterable[Any]) -> NamedListLike: if isinstance(other, NamedListLike): added = list(zip(other._names, other.objects)) elif isinstance(other, ListLike): @@ -678,7 +678,7 @@ def __setitem__(self, index: int | slice, panes: Iterable[Any]) -> None: new_objects[i], self._names[i] = self._to_object_and_name(pane) self.objects = new_objects - def clone(self, *objects: Any, **params: Any) -> 'NamedListLike': + def clone(self, *objects: Any, **params: Any) -> NamedListLike: """ Makes a copy of the Tabs sharing the same parameters. @@ -819,7 +819,7 @@ class ListPanel(ListLike, Panel): __abstract = True @property - def _linked_properties(self): + 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/card.py b/panel/layout/card.py index bd5782bf14..02d690222f 100644 --- a/panel/layout/card.py +++ b/panel/layout/card.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, Callable, ClassVar, Mapping, -) +from collections.abc import Callable, Mapping +from typing import TYPE_CHECKING, ClassVar import param diff --git a/panel/layout/feed.py b/panel/layout/feed.py index 80017899be..3976c7673c 100644 --- a/panel/layout/feed.py +++ b/panel/layout/feed.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -119,8 +118,8 @@ def _synced_range(self): return (0, min(self.load_buffer, n)) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events('scroll_button_click', model=model, doc=doc, comm=comm) @@ -153,7 +152,7 @@ def _process_param_change(self, msg): def _get_objects( self, model: Model, old_objects: list[Viewable], doc: Document, - root: Model, comm: Optional[Comm] = None + root: Model, comm: Comm | None = None ): from ..pane.base import RerenderError new_models, old_models = [], [] diff --git a/panel/layout/grid.py b/panel/layout/grid.py index 0c1e9675e1..44c3fb02d0 100644 --- a/panel/layout/grid.py +++ b/panel/layout/grid.py @@ -6,10 +6,9 @@ import math from collections import namedtuple +from collections.abc import Mapping from functools import partial -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -54,7 +53,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,...]] = () @@ -196,21 +195,22 @@ def _get_model(self, doc, root=None, parent=None, comm=None): def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: from ..io import state 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 child_name and (update_children or 'ncols' in msg or 'nrows' in msg): 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 +269,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..379ef6a285 100644 --- a/panel/layout/gridstack.py +++ b/panel/layout/gridstack.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import ClassVar, Mapping +from collections.abc import Mapping +from typing import ClassVar import param @@ -11,7 +12,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/layout/tabs.py b/panel/layout/tabs.py index e4a795d0f6..5a2e66333b 100644 --- a/panel/layout/tabs.py +++ b/panel/layout/tabs.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections import defaultdict -from typing import TYPE_CHECKING, ClassVar, Mapping +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param diff --git a/panel/links.py b/panel/links.py index d2a2853a54..baf9e53f15 100644 --- a/panel/links.py +++ b/panel/links.py @@ -7,9 +7,8 @@ import sys import weakref -from typing import ( - TYPE_CHECKING, Any, Iterable, Optional, Sequence, Union, -) +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Any, TypeAlias import param @@ -27,14 +26,14 @@ try: from holoviews.core.dimension import Dimensioned - JSLinkTarget = Union[Reactive, BkModel, 'Dimensioned'] + JSLinkTarget: TypeAlias = Reactive | BkModel | Dimensioned except Exception: - JSLinkTarget = Union[Reactive, BkModel] # type: ignore - SourceModelSpec = tuple[Optional[str], str] - TargetModelSpec = tuple[Optional[str], Optional[str]] + JSLinkTarget: TypeAlias = Reactive | BkModel # type: ignore + SourceModelSpec = tuple[str | None, str] + TargetModelSpec = tuple[str | None, str | None] -def assert_source_syncable(source: 'Reactive', properties: Iterable[str]) -> None: +def assert_source_syncable(source: Reactive, properties: Iterable[str]) -> None: for prop in properties: if prop.startswith('event:'): continue @@ -69,7 +68,7 @@ def assert_source_syncable(source: 'Reactive', properties: Iterable[str]) -> Non ) def assert_target_syncable( - source: 'Reactive', target: 'JSLinkTarget', properties: dict[str, str] + source: Reactive, target: JSLinkTarget, properties: dict[str, str] ) -> None: for k, p in properties.items(): if k.startswith('event:'): @@ -111,18 +110,18 @@ class Callback(param.Parameterized): snippet to be executed if the source property changes.""") # Mapping from a source id to a Link instance - registry: weakref.WeakKeyDictionary[Reactive | BkModel, list['Callback']] = weakref.WeakKeyDictionary() + registry: weakref.WeakKeyDictionary[Reactive | BkModel, list[Callback]] = weakref.WeakKeyDictionary() # Mapping to define callbacks by backend and Link type. # e.g. Callback._callbacks[Link] = Callback - _callbacks: dict[type['Callback'], type['CallbackGenerator']] = {} + _callbacks: dict[type[Callback], type[CallbackGenerator]] = {} # Whether the link requires a target _requires_target: bool = False def __init__( - self, source: 'Reactive', target: 'JSLinkTarget' = None, - args: dict[str, Any] = None, code: dict[str, str] = None, + self, source: Reactive, target: JSLinkTarget | None = None, + args: dict[str, Any] | None = None, code: dict[str, str] | None = None, **params ): """ @@ -183,7 +182,7 @@ def init(self) -> None: self.registry[source] = [self] @classmethod - def register_callback(cls, callback: type['CallbackGenerator']) -> None: + def register_callback(cls, callback: type[CallbackGenerator]) -> None: """ Register a LinkCallback providing the implementation for the Link for a particular backend. @@ -287,7 +286,7 @@ class Link(Callback): # Whether the link requires a target _requires_target = True - def __init__(self, source: 'Reactive', target: Optional['JSLinkTarget'] = 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 @@ -295,7 +294,7 @@ def __init__(self, source: 'Reactive', target: Optional['JSLinkTarget'] = None, super().__init__(source, **params) @property - def target(self) -> 'JSLinkTarget' | None: + def target(self) -> JSLinkTarget | None: return self._target() if self._target else None def link(self) -> None: @@ -341,8 +340,8 @@ class CallbackGenerator: error = True def __init__( - self, root_model: 'Model', link: 'Link', source: 'Reactive', - target: Optional['JSLinkTarget'] = None, arg_overrides: dict[str, Any] = {} + self, root_model: Model, link: Link, source: Reactive, + target: JSLinkTarget | None = None, arg_overrides: dict[str, Any] = {} ): self.root_model = root_model self.link = link @@ -364,8 +363,8 @@ def __init__( @classmethod def _resolve_model( - cls, root_model: 'Model', obj: 'JSLinkTarget', model_spec: str | None - ) -> 'Model' | None: + cls, root_model: Model, obj: JSLinkTarget, model_spec: str | None + ) -> Model | None: """ Resolves a model given the supplied object and a model_spec. @@ -412,9 +411,9 @@ def _resolve_model( return model def _init_callback( - self, root_model: 'Model', link: 'Link', source: 'Reactive', - src_spec: 'SourceModelSpec', target: 'JSLinkTarget' | None, - tgt_spec: 'TargetModelSpec', code: Optional[str] + self, root_model: Model, link: Link, source: Reactive, + src_spec: SourceModelSpec, target: JSLinkTarget | None, + tgt_spec: TargetModelSpec, code: str | None ) -> None: references = {k: v for k, v in link.param.values().items() if k not in ('source', 'target', 'name', 'code', 'args')} @@ -526,8 +525,8 @@ def _process_references(self, references): """ def _get_specs( - self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget' - ) -> Sequence[tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: + self, link: Link, source: Reactive, target: JSLinkTarget + ) -> Sequence[tuple[SourceModelSpec, TargetModelSpec, str | None]]: """ Return a list of spec tuples that define source and target models. @@ -535,8 +534,8 @@ def _get_specs( return [] def _get_code( - self, link: 'Link', source: 'JSLinkTarget', src_spec: str, - target: 'JSLinkTarget' | None, tgt_spec: str | None + self, link: Link, source: JSLinkTarget, src_spec: str, + target: JSLinkTarget | None, tgt_spec: str | None ) -> str: """ Returns the code to be executed. @@ -544,7 +543,7 @@ def _get_code( return '' def _get_triggers( - self, link: 'Link', src_spec: 'SourceModelSpec' + self, link: Link, src_spec: SourceModelSpec ) -> tuple[list[str], list[str]]: """ Returns the changes and events that trigger the callback. @@ -552,8 +551,8 @@ def _get_triggers( return [], [] def _initialize_models( - self, link, source: 'Reactive', src_model: 'Model', src_spec: str, - target: 'JSLinkTarget' | None, tgt_model: 'Model' | None, tgt_spec: str | None + self, link, source: Reactive, src_model: Model, src_spec: str, + target: JSLinkTarget | None, tgt_model: Model | None, tgt_spec: str | None ) -> None: """ Applies any necessary initialization to the source and target @@ -568,17 +567,18 @@ def validate(self) -> None: class JSCallbackGenerator(CallbackGenerator): def _get_triggers( - self, link: 'Link', src_spec: 'SourceModelSpec' + self, link: Link, src_spec: SourceModelSpec ) -> tuple[list[str], list[str]]: if src_spec[1].startswith('event:'): return [], [src_spec[1].split(':')[1]] return [src_spec[1]], [] def _get_specs( - self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget' - ) -> Sequence[tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: + self, link: Link, source: Reactive, target: JSLinkTarget + ) -> 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: @@ -658,8 +658,8 @@ class JSLinkCallbackGenerator(JSCallbackGenerator): """ def _get_specs( - self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget' - ) -> Sequence[tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: + self, link: Link, source: Reactive, target: JSLinkTarget + ) -> Sequence[tuple[SourceModelSpec, TargetModelSpec, str | None]]: if link.code: return super()._get_specs(link, source, target) @@ -685,8 +685,8 @@ def _get_specs( return specs def _initialize_models( - self, link, source: 'Reactive', src_model: 'Model', src_spec: str, - target: 'JSLinkTarget' | None, tgt_model: 'Model' | None, tgt_spec: str | None + self, link, source: Reactive, src_model: Model, src_spec: str, + target: JSLinkTarget | None, tgt_model: Model | None, tgt_spec: str | None ) -> None: if tgt_model is not None and src_spec and tgt_spec: src_reverse = {v: k for k, v in getattr(source, '_rename', {}).items()} @@ -721,8 +721,8 @@ def _process_references(self, references: dict[str, str]) -> None: references[k[7:]] = references.pop(k) def _get_code( - self, link: 'Link', source: 'JSLinkTarget', src_spec: str, - target: 'JSLinkTarget' | None, tgt_spec: str | None + self, link: Link, source: JSLinkTarget, src_spec: str, + target: JSLinkTarget | None, tgt_spec: str | None ) -> str: if isinstance(source, Reactive): src_reverse = {v: k for k, v in source._rename.items()} @@ -735,10 +735,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[tgt_param] 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/datetime_slider.py b/panel/models/datetime_slider.py index 665e6519b3..71f6e73dae 100644 --- a/panel/models/datetime_slider.py +++ b/panel/models/datetime_slider.py @@ -5,6 +5,6 @@ class DatetimeSlider(DateSlider): """ Slider-based datetime selection widget. """ - step = Override(default=60) + step = Override(default=60) # type: ignore - format = Override(default="%d %b %Y %H:%M:%S") + format = Override(default="%d %b %Y %H:%M:%S") # 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/icon.py b/panel/models/icon.py index 5220068808..8354f39584 100644 --- a/panel/models/icon.py +++ b/panel/models/icon.py @@ -49,7 +49,7 @@ class ToggleIcon(_ClickableIcon): """ -class ButtonIcon(_ClickableIcon, AbstractButton): +class ButtonIcon(_ClickableIcon, AbstractButton): # type: ignore """ A ButtonIcon is a clickable icon that toggles between an active and inactive state and keeps track of the number of times it has diff --git a/panel/models/trend.py b/panel/models/trend.py index f7e3697b3e..b117cc5897 100644 --- a/panel/models/trend.py +++ b/panel/models/trend.py @@ -16,8 +16,14 @@ class TrendIndicator(HTMLBox): """ description = String() - change_formatter = Instance(TickFormatter, default=lambda: NumeralTickFormatter(format='0.00%')) - formatter = Instance(TickFormatter, default=lambda: BasicTickFormatter()) + change_formatter = Instance( + TickFormatter, + default=lambda: NumeralTickFormatter(format='0.00%') # type: ignore + ) + formatter = Instance( + TickFormatter, + default=lambda: BasicTickFormatter() # type: ignore + ) layout = String() source = Instance(ColumnDataSource) plot_x = String() diff --git a/panel/models/vega.py b/panel/models/vega.py index e08f095f39..6314a813c8 100644 --- a/panel/models/vega.py +++ b/panel/models/vega.py @@ -1,6 +1,11 @@ """ Defines custom VegaPlot bokeh model to render Vega json plots. """ +from __future__ import annotations + +from typing import Literal + +from bokeh.core.enums import enumeration from bokeh.core.properties import ( Any, Bool, Dict, Enum, Instance, Int, List, Nullable, String, ) @@ -11,6 +16,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 +73,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/models/widgets.py b/panel/models/widgets.py index 615255f55a..44c1278c75 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -66,19 +66,19 @@ class Player(Widget): show_value = Bool(True, help=""" Whether to show the widget value""") - width = Override(default=400) + width = Override(default=400) # type: ignore - height = Override(default=250) + height = Override(default=250) # type: ignore scale_buttons = Float(1, help="Percentage to scale the size of the buttons by") visible_buttons = List(String, default=[ 'slower', 'first', 'previous', 'reverse', 'pause', 'play', 'next', 'last', 'faster' - ], help="The buttons to display on the player.") + ], help="The buttons to display on the player.") # type: ignore visible_loop_options = List(String, default=[ 'once', 'loop', 'reflect' - ], help="The loop options to display on the player.") + ], help="The loop options to display on the player.") # type: ignore class DiscretePlayer(Player): @@ -167,9 +167,9 @@ class VideoStream(HTMLBox): value = Any(help="""Snapshot Data""") - height = Override(default=240) + height = Override(default=240) # type: ignore - width = Override(default=320) + width = Override(default=320) # type: ignore class Progress(HTMLBox): @@ -216,7 +216,7 @@ class FileDownload(InputWidget): A private property to create and click the link. """) - title = Override(default='') + title = Override(default='') # type: ignore class CustomSelect(Select): diff --git a/panel/pane/alert.py b/panel/pane/alert.py index b2d390bf0d..1266b4799c 100644 --- a/panel/pane/alert.py +++ b/panel/pane/alert.py @@ -5,7 +5,8 @@ """ from __future__ import annotations -from typing import Any, ClassVar, Mapping +from collections.abc import Mapping +from typing import Any, ClassVar import param diff --git a/panel/pane/base.py b/panel/pane/base.py index 1f5c758b70..2ec5b00aca 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -4,9 +4,10 @@ """ from __future__ import annotations +from collections.abc import Callable, Mapping from functools import partial from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, TypeVar, + TYPE_CHECKING, Any, ClassVar, TypeVar, ) import numpy as np @@ -40,7 +41,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. @@ -142,7 +143,7 @@ class PaneBase(Layoutable): # Whether applies requires full set of keywords _applies_kw: ClassVar[bool] = False - _skip_layoutable = ('css_classes', 'margin', 'name') + _skip_layoutable: tuple[str, ...] = ('css_classes', 'margin', 'name') # Whether the Pane layout can be safely unpacked _unpack: ClassVar[bool] = True @@ -231,7 +232,7 @@ def applies(cls, obj: Any) -> float | bool | None: return None @classmethod - def get_pane_type(cls, obj: Any, **kwargs) -> type['PaneBase']: + def get_pane_type(cls, obj: Any, **kwargs) -> type[PaneBase]: """ Returns the applicable Pane type given an object by resolving the precedence of all types whose applies method declares that @@ -317,7 +318,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 @@ -340,7 +341,7 @@ def _param_change(self, *events: param.parameterized.Event) -> None: super()._param_change(*events) def _update_object( - self, ref: str, doc: 'Document', root: Model, parent: Model, comm: Comm | None + self, ref: str, doc: Document, root: Model, parent: Model, comm: Comm | None ) -> None: old_model = self._models[ref][0] if self._updates: @@ -366,14 +367,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 +383,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 +415,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 +446,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: @@ -462,7 +463,7 @@ def _get_root_model( # Public API #---------------------------------------------------------------- - def clone(self: T, object: Optional[Any] = None, **params) -> T: + def clone(self: T, object: Any | None = None, **params) -> T: """ Makes a copy of the Pane sharing the same parameters. @@ -483,7 +484,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 +530,7 @@ class ModelPane(Pane): `bokeh.model.Model` can consume. """ - _bokeh_model: ClassVar[Model] + _bokeh_model: ClassVar[type[Model] | None] = None __abstract = True @@ -537,6 +538,10 @@ def _get_model( self, doc: Document, root: Model | None = None, parent: Model | None = None, comm: Comm | None = None ) -> Model: + if self._bokeh_model is None: + raise NotImplementedError( + f'Pane {type(self).__name__} did not define a _bokeh_model' + ) model = self._bokeh_model(**self._get_properties(doc)) if root is None: root = model @@ -591,11 +596,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 +656,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/deckgl.py b/panel/pane/deckgl.py index 7042f400e2..091674028d 100644 --- a/panel/pane/deckgl.py +++ b/panel/pane/deckgl.py @@ -8,9 +8,8 @@ import sys from collections import defaultdict -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -157,7 +156,7 @@ def _process_data(cls, data): return {col: np.asarray(vals) for col, vals in columns.items()} @classmethod - def _update_sources(cls, json_data, sources): + def _update_sources(cls, json_data, sources: list[ColumnDataSource]): layers = json_data.get('layers', []) # Create index of sources by columns @@ -276,19 +275,20 @@ def _transform_object(self, obj) -> dict[str, Any]: return dict(data=data, tooltip=tooltip, configuration=configuration, mapbox_api_key=mapbox_api_key or "") def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = DeckGLPlot = lazy_load( + DeckGL._bokeh_model = lazy_load( 'panel.models.deckgl', 'DeckGLPlot', isinstance(comm, JupyterComm), root ) properties = self._get_properties(doc) data = properties.pop('data') - properties['data_sources'] = sources = [] + sources: list[ColumnDataSource] = [] + properties['data_sources'] = sources self._update_sources(data, sources) properties['layers'] = data.pop('layers', []) properties['initialViewState'] = data.pop('initialViewState', {}) - model = DeckGLPlot(data=data, **properties) + model = DeckGL._bokeh_model(data=data, **properties) root = root or model self._link_props(model, ['clickState', 'hoverState', 'viewState'], doc, root, comm) self._models[root.ref["id"]] = (model, parent) diff --git a/panel/pane/echarts.py b/panel/pane/echarts.py index 22fd8d9ba1..915afb719e 100644 --- a/panel/pane/echarts.py +++ b/panel/pane/echarts.py @@ -4,9 +4,8 @@ import sys from collections import defaultdict -from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, -) +from collections.abc import Callable, Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -116,7 +115,7 @@ def _process_param_change(self, params): props['sizing_mode'] = 'stretch_both' return props - def _get_properties(self, document: Document): + def _get_properties(self, document: Document | None) -> dict[str, Any]: props = super()._get_properties(document) props['event_config'] = { event: list(queries) for event, queries in self._py_callbacks.items() @@ -124,10 +123,10 @@ def _get_properties(self, document: Document): return props def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = lazy_load( + ECharts._bokeh_model = lazy_load( 'panel.models.echarts', 'ECharts', isinstance(comm, JupyterComm), root ) model = super()._get_model(doc, root, parent, comm) diff --git a/panel/pane/equation.py b/panel/pane/equation.py index 4af31999ce..2eaf9f02bb 100644 --- a/panel/pane/equation.py +++ b/panel/pane/equation.py @@ -6,9 +6,8 @@ import sys -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param # type: ignore @@ -68,7 +67,7 @@ def applies(cls, obj: Any) -> float | bool | None: else: return False - def _get_model_type(self, root: Model, comm: Comm | None) -> type[Model]: + def _get_model_type(self, root: Model | None, comm: Comm | None) -> type[Model]: module = self.renderer if module is None: if 'panel.models.mathjax' in sys.modules and 'panel.models.katex' not in sys.modules: @@ -83,8 +82,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/holoviews.py b/panel/pane/holoviews.py index 827d45eff8..a10fae6439 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -8,10 +8,9 @@ import sys from collections import defaultdict +from collections.abc import Mapping from functools import partial -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -417,8 +416,8 @@ def _process_param_change(self, params): #---------------------------------------------------------------- def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: from holoviews.plotting.plot import Plot if root is None: @@ -738,8 +737,8 @@ def _update_layout_properties(self, *events): self._layout_panel.param.update(**{e.name: e.new for e in events}) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: if root is None: return self.get_root(doc, comm) diff --git a/panel/pane/image.py b/panel/pane/image.py index e98c0f60db..0ca141557e 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -8,11 +8,10 @@ import base64 import struct +from collections.abc import Mapping from io import BytesIO from pathlib import PurePath -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, -) +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -119,6 +118,7 @@ async def replace_content(): import requests r = requests.request(url=obj, method='GET') return r.content + return None class ImageBase(FileBase): @@ -205,6 +205,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 +445,8 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: if self.embed or (isfile(obj) or (isinstance(obj, str) and obj.lstrip().startswith(' Model: if root is None: return self.get_root(doc, comm) @@ -88,7 +86,7 @@ class IPyLeaflet(IPyWidget): 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) - priority: float | bool | None = 0.7 + priority: ClassVar[float | bool | None] = 0.7 @classmethod def applies(cls, obj: Any) -> float | bool | None: @@ -115,7 +113,7 @@ def _cleanup(self, root: Model | None = None) -> None: super()._cleanup(root) def _get_ipywidget( - self, obj, doc: Document, root: Model, comm: Optional[Comm], **kwargs + self, obj, doc: Document, root: Model, comm: Comm | None, **kwargs ): if not isinstance(comm, JupyterComm) or "PANEL_IPYWIDGET" in os.environ: from ..io.ipywidget import Widget # noqa @@ -125,7 +123,7 @@ def _get_ipywidget( return super()._get_ipywidget(widget, doc, root, comm, **kwargs) -_ipywidget_classes = {} +_ipywidget_classes: dict[str, type[param.Parameterized]] = {} def _ipywidget_transform(obj): """ diff --git a/panel/pane/markup.py b/panel/pane/markup.py index 30a1b9c7be..17640d7e3f 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -8,9 +8,8 @@ import json import textwrap -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param # type: ignore @@ -36,7 +35,7 @@ class HTMLBasePane(ModelPane): Whether to enable streaming of text snippets. This is useful when updating a string step by step, e.g. in a chat message.""") - _bokeh_model: ClassVar[Model] = _BkHTML + _bokeh_model: ClassVar[type[Model]] = _BkHTML _rename: ClassVar[Mapping[str, str | None]] = {'object': 'text', 'enable_streaming': None} @@ -271,8 +270,8 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: if 'dask' in module: html = obj.to_html(max_rows=self.max_rows).replace('border="1"', '') elif 'style' in module: - classes = ' '.join(classes) - html = obj.to_html(table_attributes=f'class="{classes}"') + class_string = ' '.join(classes) + html = obj.to_html(table_attributes=f'class="{class_string}"') else: kwargs = {p: getattr(self, p) for p in self._rerender_params if p not in HTMLBasePane.param and p not in ('_object', 'text_align')} @@ -397,7 +396,7 @@ def applies(cls, obj: Any) -> float | bool | None: return False @classmethod - @functools.lru_cache(maxsize=None) + @functools.cache def _get_parser(cls, renderer, plugins, **renderer_options): if renderer == 'markdown': return None @@ -461,7 +460,7 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: html = markdown.markdown( obj, extensions=self.extensions, - output_format='html5', + output_format='xhtml', **self.renderer_options ) else: @@ -512,7 +511,7 @@ class JSON(HTMLBasePane): _applies_kw: ClassVar[bool] = True - _bokeh_model: ClassVar[Model] = _BkJSON + _bokeh_model: ClassVar[type[Model]] = _BkJSON _rename: ClassVar[Mapping[str, str | None]] = { "object": "text", "encoder": None, "style": "styles" diff --git a/panel/pane/media.py b/panel/pane/media.py index e510dc3b82..4777aac43c 100644 --- a/panel/pane/media.py +++ b/panel/pane/media.py @@ -6,8 +6,9 @@ import os from base64 import b64encode +from collections.abc import Mapping from io import BytesIO -from typing import Any, ClassVar, Mapping +from typing import Any, ClassVar import numpy as np import param @@ -89,7 +90,7 @@ def applies(cls, obj: Any) -> float | bool | None: return True return False - def _to_np_int16(self, data: np.ndarray): + def _to_np_int16(self, data: np.ndarray) -> np.ndarray: dtype = data.dtype if dtype in (np.float32, np.float64): @@ -97,10 +98,12 @@ def _to_np_int16(self, data: np.ndarray): return data - def _to_buffer(self, data: np.ndarray|TensorLike): - if isinstance(data, TensorLike): - data = data.numpy() - data = self._to_np_int16(data) + def _to_buffer(self, data: np.ndarray | TensorLike): + if isinstance(data, np.ndarray): + values = data + elif isinstance(data, TensorLike): + values = data.numpy() + data = self._to_np_int16(values) from scipy.io import wavfile buffer = BytesIO() diff --git a/panel/pane/perspective.py b/panel/pane/perspective.py index 476a9e3904..5acbdaefab 100644 --- a/panel/pane/perspective.py +++ b/panel/pane/perspective.py @@ -3,11 +3,10 @@ import datetime as dt import sys +from collections.abc import Callable, Mapping from enum import Enum from functools import partial -from typing import ( - TYPE_CHECKING, Callable, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, ClassVar import numpy as np import param @@ -364,7 +363,7 @@ def _get_data(self): k: v for k, v in kwargs.items() if getattr(self, k) is None }) - cols = set(self._as_digit(c) for c in df) + cols = {self._as_digit(c) for c in df} if len(cols) != ncols: raise ValueError("Integer columns must be unique when " "converted to strings.") @@ -475,10 +474,10 @@ def _process_property_change(self, msg): return msg def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = lazy_load( + Perspective._bokeh_model = lazy_load( 'panel.models.perspective', 'Perspective', isinstance(comm, JupyterComm), root ) model = super()._get_model(doc, root, parent, comm) diff --git a/panel/pane/plot.py b/panel/pane/plot.py index 8b83769eb7..eecbdd0f27 100644 --- a/panel/pane/plot.py +++ b/panel/pane/plot.py @@ -6,12 +6,11 @@ import re import sys +from collections.abc import Mapping from contextlib import contextmanager from functools import partial from io import BytesIO -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -162,8 +161,8 @@ def _sync_properties(self): self._syncing_props = False def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: if root is None: return self.get_root(doc, comm) @@ -319,8 +318,8 @@ def _transform_object(self, obj: Any) -> dict[str, Any]: return self._img_type._transform_object(self, obj) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: if not self.interactive: return self._img_type._get_model(self, doc, root, parent, comm) diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index 8b15d81439..5ffd31fef7 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -4,9 +4,8 @@ """ from __future__ import annotations -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -311,13 +310,12 @@ def _process_param_change(self, params): return props def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - if not hasattr(self, '_bokeh_model'): - self._bokeh_model = lazy_load( - 'panel.models.plotly', 'PlotlyPlot', isinstance(comm, JupyterComm), root - ) + Plotly._bokeh_model = lazy_load( + 'panel.models.plotly', 'PlotlyPlot', isinstance(comm, JupyterComm), root + ) model = super()._get_model(doc, root, parent, comm) self._register_events('plotly_event', model=model, doc=doc, comm=comm) return model @@ -381,7 +379,7 @@ def _update(self, ref: str, model: Model) -> None: except Exception: update_frames = True - updates = {} + updates: dict[str, Any] = {} if self.sizing_mode is self.param.sizing_mode.default and 'autosize' in layout: autosize = layout.get('autosize') styles = dict(model.styles) diff --git a/panel/pane/streamz.py b/panel/pane/streamz.py index c4cbc63641..f1dcc4e7f9 100644 --- a/panel/pane/streamz.py +++ b/panel/pane/streamz.py @@ -5,9 +5,8 @@ import sys -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -58,8 +57,8 @@ def _setup_stream(self): self._stream.sink(self._update_inner) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._setup_stream() diff --git a/panel/pane/vega.py b/panel/pane/vega.py index 3ad207eeba..f61a5bf059 100644 --- a/panel/pane/vega.py +++ b/panel/pane/vega.py @@ -3,9 +3,8 @@ import re import sys -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -32,7 +31,7 @@ def ds_as_cds(dataset): if len(dataset) == 0: return {} # create a list of unique keys from all items as some items may not include optional fields - keys = sorted(set(k for d in dataset for k in d.keys())) + keys = sorted({k for d in dataset for k in d.keys()}) data = {k: [] for k in keys} for item in dataset: for k in keys: @@ -286,10 +285,10 @@ def _get_properties(self, doc, sources={}): return props def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = lazy_load( + Vega._bokeh_model = lazy_load( 'panel.models.vega', 'VegaPlot', isinstance(comm, JupyterComm), root ) model = super()._get_model(doc, root, parent, comm) diff --git a/panel/pane/vizzu.py b/panel/pane/vizzu.py index 38be1ca570..e1f7d3931c 100644 --- a/panel/pane/vizzu.py +++ b/panel/pane/vizzu.py @@ -3,9 +3,8 @@ import datetime as dt import sys -from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Optional, -) +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -149,10 +148,10 @@ def _process_param_change(self, params): return super()._process_param_change(params) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._bokeh_model = lazy_load( + Vizzu._bokeh_model = lazy_load( 'panel.models.vizzu', 'VizzuChart', isinstance(comm, JupyterComm), root ) model = super()._get_model(doc, root, parent, comm) diff --git a/panel/pane/vtk/enums.py b/panel/pane/vtk/enums.py index 69a5b62dcd..fc5bc7a8e0 100644 --- a/panel/pane/vtk/enums.py +++ b/panel/pane/vtk/enums.py @@ -14,11 +14,11 @@ class TextPosition(Enum): SCALAR_MODE = namedtuple("SCALAR_MODE", "Default UsePointData UseCellData UsePointFieldData UseCellFieldData UseFieldData" -)(0, 1, 2, 3, 4, 5) +)(0, 1, 2, 3, 4, 5) # type: ignore -COLOR_MODE = namedtuple("COLOR_MODE", "DirectScalars MapScalars")(0, 1) +COLOR_MODE = namedtuple("COLOR_MODE", "DirectScalars MapScalars")(0, 1) # type: ignore -ACCESS_MODE = namedtuple("ACCESS_MODE", "ById ByName")(0, 1) +ACCESS_MODE = namedtuple("ACCESS_MODE", "ById ByName")(0, 1) # type: ignore PRESET_CMAPS = [ 'KAAMS', diff --git a/panel/pane/vtk/synchronizable_serializer.py b/panel/pane/vtk/synchronizable_serializer.py index 9d9d2668c6..9f0c35db82 100644 --- a/panel/pane/vtk/synchronizable_serializer.py +++ b/panel/pane/vtk/synchronizable_serializer.py @@ -5,6 +5,8 @@ import time import zipfile +from typing import Any + from vtk.vtkCommonCore import vtkTypeInt32Array, vtkTypeUInt32Array from vtk.vtkCommonDataModel import vtkDataObject from vtk.vtkFiltersGeometry import ( @@ -410,7 +412,7 @@ def getReferenceId(ref): # ----------------------------------------------------------------------------- -dataArrayShaMapping = {} +dataArrayShaMapping: dict[str, dict[str, Any]] = {} def digest(array): diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index 99b22ec452..e9ef33f976 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -9,8 +9,9 @@ import zipfile from abc import abstractmethod +from collections.abc import Mapping from typing import ( - IO, TYPE_CHECKING, Any, ClassVar, Mapping, Optional, + IO, TYPE_CHECKING, Any, ClassVar, ) from urllib.request import urlopen @@ -83,7 +84,7 @@ def _process_param_change(self, msg): def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: if 'axes' in msg and msg['axes'] is not None: VTKAxes = sys.modules['panel.models.vtk'].VTKAxes @@ -376,14 +377,14 @@ def applies(cls, obj, **kwargs): serialize_on_instantiation) def __init__(self, object=None, **params): - super(VTKRenderWindow, self).__init__(object, **params) + super().__init__(object, **params) if object is not None: self.color_mappers = self.get_color_mappers() self._update(None, None) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: VTKSynchronizedPlot = lazy_load( 'panel.models.vtk', 'VTKSynchronizedPlot', isinstance(comm, JupyterComm), root @@ -443,8 +444,8 @@ def __init__(self, object=None, **params): self._contexts = {} def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: VTKSynchronizedPlot = lazy_load( 'panel.models.vtk', 'VTKSynchronizedPlot', isinstance(comm, JupyterComm), root @@ -468,8 +469,9 @@ def _get_model( return model def _cleanup(self, root: Model | None = None) -> None: - ref = root.ref['id'] - self._contexts.pop(ref, None) + if root: + ref = root.ref['id'] + self._contexts.pop(ref, None) super()._cleanup(root) def _update(self, ref: str, model: Model) -> None: @@ -638,7 +640,7 @@ class VTKVolume(AbstractVTK): Integer parameter to control the position of the slice normal to the Z direction.""") - _serializers = {} + _serializers: dict[type, Any] = {} _rename: ClassVar[Mapping[str, str | None]] = {'max_data_size': None, 'spacing': None, 'origin': None} @@ -661,8 +663,8 @@ def applies(cls, obj: Any) -> float | bool | None: return isinstance(obj, vtk.vtkImageData) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: VTKVolumePlot = lazy_load( 'panel.models.vtk', 'VTKVolumePlot', isinstance(comm, JupyterComm), root @@ -733,7 +735,7 @@ def register_serializer(cls, class_type, serializer): A serializer is a function which take an instance of `class_type` (like a vtk.vtkImageData) as input and return a numpy array of the data """ - cls._serializers.update({class_type:serializer}) + cls._serializers.update({class_type: serializer}) def _volume_from_array(self, sub_array): return dict( @@ -820,7 +822,7 @@ class VTKJS(AbstractVTK): notebook context if they interact with already bound keys.""") - _serializers = {} + _serializers: dict[type, Any] = {} _updates = True @@ -832,10 +834,11 @@ def __init__(self, object=None, **params): def applies(cls, obj: Any) -> float | bool | None: if isinstance(obj, str) and obj.endswith('.vtkjs'): return True + return None def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: """ Should return the bokeh model to be rendered. diff --git a/panel/param.py b/panel/param.py index 2e60e87a41..42c64956d2 100644 --- a/panel/param.py +++ b/panel/param.py @@ -14,19 +14,17 @@ import types from collections import defaultdict, namedtuple -from collections.abc import Callable +from collections.abc import Callable, Generator from contextlib import contextmanager from functools import partial -from typing import ( - TYPE_CHECKING, Any, ClassVar, Generator, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import param try: from param import Skip except Exception: - class Skip(Exception): + class Skip(Exception): # type: ignore """ Exception that allows skipping an update for function-level updates. """ @@ -36,14 +34,6 @@ class Skip(Exception): ) from param.reactive import rx -try: - from param import Skip -except Exception: - class Skip(RuntimeError): - """ - Exception that allows skipping an update for function-level updates. - """ - from .config import config from .io import state from .layout import ( @@ -85,13 +75,13 @@ def SingleFileSelector(pobj: param.Parameter) -> type[Widget]: def LiteralInputTyped(pobj: param.Parameter) -> type[Widget]: if isinstance(pobj, param.Tuple): - return type(str('TupleInput'), (LiteralInput,), {'type': tuple}) + return type('TupleInput', (LiteralInput,), {'type': tuple}) elif isinstance(pobj, param.Number): - return type(str('NumberInput'), (LiteralInput,), {'type': (int, float)}) + return type('NumberInput', (LiteralInput,), {'type': (int, float)}) elif isinstance(pobj, param.Dict): - return type(str('DictInput'), (LiteralInput,), {'type': dict}) + return type('DictInput', (LiteralInput,), {'type': dict}) elif isinstance(pobj, param.List): - return type(str('ListInput'), (LiteralInput,), {'type': list}) + return type('ListInput', (LiteralInput,), {'type': list}) return LiteralInput @@ -214,7 +204,7 @@ class Param(Pane): Dictionary of widget overrides, mapping from parameter name to widget class.""") - mapping: ClassVar[Mapping[param.Parameter, Widget | Callable[[param.Parameter], Widget]]] = { + mapping: ClassVar[dict[param.Parameter, type[WidgetBase] | Callable[[param.Parameter], type[WidgetBase]]]] = { param.Action: Button, param.Array: ArrayInput, param.Boolean: Checkbox, @@ -723,10 +713,11 @@ def _get_widgets(self): return dict(widgets) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = self.layout._get_model(doc, root, parent, comm) + root = root or model self._models[root.ref['id']] = (model, parent) return model @@ -758,7 +749,7 @@ def widget_type(cls, pobj): return wtype 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: root = super().get_root(doc, comm, preprocess) @@ -920,8 +911,8 @@ def _update_pane(self, *events): self._replace_pane() def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: if not self._evaled: deferred = self.defer_load and not state.loaded @@ -1237,9 +1228,9 @@ def widgets(self): return self.widget_layout(*widgets) 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: return self.layout._get_model(doc, root, parent, comm) def _generate_layout(self): @@ -1312,7 +1303,7 @@ def __call__(self, parameterized): if self.json_file or env_var.endswith('.json'): try: fname = self.json_file if self.json_file else env_var - with open(fullpath(fname), 'r') as f: + with open(fullpath(fname)) as f: spec = json.load(f) except Exception: warnobj.warning(f'Could not load JSON file {spec!r}') diff --git a/panel/reactive.py b/panel/reactive.py index 91a38f52b8..340a59c916 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -16,10 +16,11 @@ import textwrap from collections import Counter, defaultdict, namedtuple +from collections.abc import Callable, Mapping, Sequence from functools import lru_cache, partial from pprint import pformat from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, Union, + TYPE_CHECKING, Any, ClassVar, TypeAlias, ) import jinja2 @@ -60,11 +61,15 @@ from bokeh.events import Event from bokeh.model import Model, ModelEvent from bokeh.models.sources import DataDict, Patches + from pandas.api.extensions import ExtensionArray from pyviz_comms import Comm from .layout.base import Panel as BasePanel from .links import Callback, JSLinkTarget, Link + TData: TypeAlias = pd.DataFrame | DataDict + TDataColumn: TypeAlias = np.ndarray | pd.Series | pd.Index | ExtensionArray | Sequence[Any] + log = logging.getLogger('panel.reactive') _fields = tuple(Watcher._fields+('target', 'links', 'transformed', 'bidirectional_watcher')) @@ -115,7 +120,7 @@ class Syncable(Renderable): _stylesheets: ClassVar[list[str]] = [] # Property changes that should not trigger busy indicator - _busy__ignore = [] + _busy__ignore: ClassVar[list[str]] = [] __abstract = True @@ -163,13 +168,13 @@ def _property_mapping(cls): return rename @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 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 +220,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,8 +265,8 @@ def _link_params(self) -> None: self._internal_callbacks.append(watcher) def _link_props( - self, model: Model, properties: list[str] | list[tuple[str, str]], - doc: Document, root: Model, comm: Optional[Comm] = None + self, model: Model | DataModel, properties: Sequence[str] | Sequence[tuple[str, str]], + doc: Document, root: Model, comm: Comm | None = None ) -> None: from .config import config ref = root.ref['id'] @@ -270,9 +278,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: @@ -282,7 +291,7 @@ def _link_props( def _manual_update( self, events: tuple[param.parameterized.Event, ...], model: Model, doc: Document, - root: Model, parent: Optional[Model], comm: Optional[Comm] + root: Model, parent: Model | None, comm: Comm | None ) -> None: """ Method for handling any manual update events, i.e. events triggered @@ -308,7 +317,7 @@ def _update_manual(self, *events: param.parameterized.Event) -> None: def _scheduled_update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm], + root: Model, model: Model, doc: Document, comm: Comm | None, curdoc_events: dict[str, Any] ) -> None: # @@ -339,7 +348,7 @@ def _apply_update( def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: ref = root.ref['id'] self._changing[ref] = attrs = [] @@ -378,7 +387,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 +537,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 +580,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 +658,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 +679,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 @@ -688,7 +698,7 @@ def _update_properties(self, *events: param.parameterized.Event, doc: Document) def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: if 'stylesheets' in msg: if doc and 'dist_url' in doc._template_variables: @@ -705,8 +715,8 @@ def _update_model( #---------------------------------------------------------------- def link( - self, target: param.Parameterized, callbacks: Optional[dict[str, str | Callable]]=None, - bidirectional: bool=False, **links: str + self, target: param.Parameterized, callbacks: dict[str, str | Callable] | None=None, + bidirectional: bool = False, **links: str ) -> Watcher: """ Links the parameters on this `Reactive` object to attributes on the @@ -863,8 +873,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 +929,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 +958,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 +993,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,8 +1006,9 @@ 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: + def _update_column(self, column: str, array: TDataColumn) -> None: """ Implemented by subclasses converting changes in columns to changes in the data parameter. @@ -1018,7 +1030,7 @@ def _update_data(self, data: TData) -> None: def _manual_update( self, events: tuple[param.parameterized.Event, ...], model: Model, - doc: Document, root: Model, parent: Optional[Model], comm: Comm + doc: Document, root: Model, parent: Model | None, comm: Comm ) -> None: for event in events: if event.type == 'triggered' and self._updating: @@ -1036,7 +1048,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 +1056,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 +1064,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 +1079,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 +1087,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 +1113,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 +1177,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 +1208,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: @@ -1210,7 +1222,7 @@ def stream( else: raise ValueError("The stream value provided is not a DataFrame, Series or Dict!") - def patch(self, patch_value: 'pd.DataFrame' | 'pd.Series' | dict) -> None: + def patch(self, patch_value: pd.DataFrame | pd.Series | dict) -> None: """ Efficiently patches (updates) the existing value with the `patch_value`. @@ -1277,12 +1289,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): @@ -1316,25 +1332,26 @@ def _update_selection(self, indices: list[int]) -> None: self.selection = indices def _convert_column( - self, values: np.ndarray, old_values: np.ndarray | 'pd.Series' - ) -> np.ndarray | list: - dtype = old_values.dtype - converted: list | np.ndarray | None = None + self, values: np.ndarray, old_values: TDataColumn + ) -> TDataColumn: + dtype = getattr(old_values, 'dtype', np.dtype('O')) + converted: TDataColumn | None = None if dtype.kind == 'M': if values.dtype.kind in 'if': - if getattr(dtype, 'tz', None): + tz = getattr(dtype, 'tz', None) + if tz: import pandas as pd # Using pandas to convert from milliseconds # timezone-aware, to UTC nanoseconds, to datetime64. converted = ( pd.Series(pd.to_datetime(values, unit="ms")) - .dt.tz_localize(dtype.tz) + .dt.tz_localize(tz) ) else: # Timestamps converted from milliseconds to nanoseconds, # to datetime. - converted = (values * 1e6).astype(dtype) + converted = (values * 1e6).astype(dtype) # type: ignore elif dtype.kind == 'O': if (all(isinstance(ov, dt.date) for ov in old_values) and not all(isinstance(iv, dt.date) for iv in values)): @@ -1348,13 +1365,16 @@ def _convert_column( converted = new_values elif 'pandas' in sys.modules: import pandas as pd + tmp_values: np.ndarray | list[Any] | pd.api.extensions.ExtensionArray = values 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 + ] + converted = pd.Series(tmp_values).astype(dtype).values else: - converted = values.astype(dtype) + converted = values.astype(dtype) # type: ignore return values if converted is None else converted def _process_data(self, data: Mapping[str, list | dict[int, Any] | np.ndarray]) -> None: @@ -1376,23 +1396,21 @@ def _process_data(self, data: Mapping[str, list | dict[int, Any] | np.ndarray]) if isinstance(values, dict): sorted_values = sorted(values.items(), key=lambda it: int(it[0])) values = [v for _, v in sorted_values] - values = self._convert_column( - np.asarray(values), old_raw[col] - ) + converted = self._convert_column(np.asarray(values), old_raw[col]) isequal = None - if hasattr(old_raw, 'columns') and isinstance(values, np.ndarray): + if hasattr(old_raw, 'columns') and isinstance(converted, np.ndarray): try: - isequal = np.array_equal(old_raw[col], values, equal_nan=True) + isequal = np.array_equal(old_raw[col], converted, equal_nan=True) except Exception: pass if isequal is None: try: - isequal = (old_raw[col] == values).all() # type: ignore + isequal = (old_raw[col] == converted).all() # type: ignore except Exception: isequal = False if not isequal: - self._update_column(col, values) + self._update_column(col, converted) updated = True # If no columns were updated we don't have to sync data @@ -1426,7 +1444,7 @@ def _process_events(self, events: dict[str, Any]) -> None: self._update_selection(events.pop('indices')) finally: self._updating = False - super(ReactiveData, self)._process_events(events) + super()._process_events(events) # noqa class ReactiveMetaBase(ParameterizedMetaclass): @@ -1547,11 +1565,11 @@ def __init__(mcs, name: str, bases: tuple[type, ...], dict_: Mapping[str, Any]): class ReactiveCustomBase(Reactive): - _extension_name: ClassVar[Optional[str]] = None + _extension_name: ClassVar[str | None] = None - __css__: ClassVar[Optional[list[str]]] = None - __javascript__: ClassVar[Optional[list[str]]] = None - __javascript_modules__: ClassVar[Optional[list[str]]] = None + __css__: ClassVar[list[str] | None] = None + __javascript__: ClassVar[list[str] | None] = None + __javascript_modules__: ClassVar[list[str] | None] = None @classmethod def _loaded(cls) -> bool: @@ -1825,7 +1843,7 @@ def _child_names(self): return {} def _process_children( - self, doc: Document, root: Model, model: Model, comm: Optional[Comm], + self, doc: Document, root: Model, model: Model, comm: Comm | None, children: dict[str, list[Model]] ) -> dict[str, list[Model]]: return children @@ -1902,7 +1920,7 @@ def _get_events(self) -> dict[str, dict[str, bool]]: return events def _get_children( - self, doc: Document, root: Model, model: Model, comm: Optional[Comm] + self, doc: Document, root: Model, model: Model, comm: Comm | None ) -> dict[str, list[Model]]: from .pane import panel old_models = model.children @@ -2036,7 +2054,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) -> list[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): @@ -2066,8 +2084,8 @@ def _patch_datamodel_ref(cls, props, ref): props.tags.append(f"__ref:{ref}") def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = _BkReactiveHTML(**self._get_properties(doc)) if comm and not self._loaded(): @@ -2102,7 +2120,7 @@ def _get_model( self._models[root.ref['id']] = (model, parent) return model - def _process_event(self, event: 'Event') -> None: + def _process_event(self, event: Event) -> None: if not isinstance(event, DOMEvent): return cb = getattr(self, f"_{event.node}_{event.data['type']}", None) @@ -2132,7 +2150,7 @@ def _set_on_model(self, msg: Mapping[str, Any], root: Model, model: Model) -> No def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: child_params = self._parser.children.values() new_children, model_msg, data_msg = {}, {}, {} diff --git a/panel/template/base.py b/panel/template/base.py index a47161dc5e..7d30fd1209 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -11,7 +11,7 @@ from functools import partial from pathlib import Path, PurePath from typing import ( - IO, TYPE_CHECKING, Any, ClassVar, Literal, Optional, + IO, TYPE_CHECKING, Any, ClassVar, Literal, ) import jinja2 @@ -57,7 +57,7 @@ from jinja2 import Template as _Template from pyviz_comms import Comm - from ..io.location.base import Location + from ..io.location import Location from ..io.resources import ResourcesType @@ -97,13 +97,13 @@ class BaseTemplate(param.Parameterized, MimeRenderMixin, ServableMixin, Resource ############# # pathlib.Path pointing to local CSS file(s) - _css: ClassVar[Path | str | list[Path | str] | None] = None + _css: ClassVar[list[Path | str]] = [] # pathlib.Path pointing to local JS file(s) _js: ClassVar[Path | str | list[Path | str] | None] = None # External resources - _resources: ClassVar[dict[str, dict[str, str]]] = { + _resources = { 'css': {}, 'js': {}, 'js_modules': {}, 'tarball': {} } @@ -111,7 +111,7 @@ class BaseTemplate(param.Parameterized, MimeRenderMixin, ServableMixin, Resource def __init__( self, template: str | _Template, items=None, - nb_template: Optional[str | _Template] = None, **params + nb_template: str | _Template | None = None, **params ): config_params = { p: v for p, v in params.items() if p in _base_config.param @@ -186,13 +186,12 @@ def _server_destroy(self, session_context: BokehSessionContext): self._documents.remove(doc) def _init_doc( - self, doc: Optional[Document] = None, comm: Optional[Comm] = None, - title: Optional[str] = None, notebook: bool = False, + self, doc: Document | None = None, comm: Comm | None = None, + title: str | None = None, notebook: bool = False, location: bool | Location = True ): # Initialize document - document: Document = doc or curdoc_locked() - document = init_doc(document) + document = init_doc(doc or curdoc_locked()) self._documents.append(document) if document not in state._templates: @@ -217,7 +216,7 @@ def _init_doc( # Add all render items to the document objs, models = [], [] stylesheets, sizing_modes = {}, {} - tracked_models = set() + tracked_models: set[Model] = set() for name, (obj, tags) in self._render_items.items(): # Render root without pre-processing with config.set(design=self.design): @@ -372,10 +371,10 @@ def resolve_resources( )]) for rname, res in self._design.resolve_resources(cdn).items(): if isinstance(res, dict): - resource_types[rname].update(res) + resource_types[rname].update(res) # type: ignore else: - resource_types[rname] += [ - r for r in res if res not in resource_types[rname] + resource_types[rname] += [ # type: ignore + r for r in res if res not in resource_types[rname] # type: ignore ] for rname, js in self.config.js_files.items(): @@ -402,7 +401,7 @@ def resolve_resources( continue elif scls._css is None: break - tmpl_css = scls._css if isinstance(scls._css, list) else [scls._css] + tmpl_css = scls._css if isinstance(scls._css, list) else [scls._css] # type: ignore if css in tmpl_css: tmpl_name = scls.__name__.lower() @@ -439,10 +438,10 @@ def resolve_resources( return resource_types def save( - self, filename: str | os.PathLike | IO, title: Optional[str] = None, + self, filename: str | os.PathLike | IO, title: str | None = None, resources=None, embed: bool = False, max_states: int = 1000, max_opts: int = 3, embed_json: bool = False, json_prefix: str='', - save_path: str='./', load_path: Optional[str] = None + save_path: str='./', load_path: str | None = None ) -> None: """ Saves Panel objects to file. @@ -480,7 +479,7 @@ def save( ) def server_doc( - self, doc: Optional[Document] = None, title: str = None, + self, doc: Document | None = None, title: str | None = None, location: bool | Location = True ) -> Document: """ @@ -505,9 +504,9 @@ def server_doc( return self._init_doc(doc, title=title, location=location) def servable( - self, title: Optional[str] = None, location: bool | Location = True, - area: str = 'main', target: Optional[str] = None - ) -> BaseTemplate: + self, title: str | None = None, location: bool | Location = True, + area: str = 'main', target: str | None = None + ) -> ServableMixin: """ Serves the template and returns self to allow it to display itself in a notebook context. @@ -532,7 +531,8 @@ def servable( ------- The template object """ - if curdoc_locked().session_context and config.template: + doc = curdoc_locked() + if doc and doc.session_context and config.template: raise RuntimeError( 'Cannot mark template as servable if a global template ' 'is defined. Either explicitly construct a template and ' @@ -751,8 +751,8 @@ def __init__(self, **params): self.modal.param.trigger('objects') def _init_doc( - self, doc: Optional[Document] = None, comm: Optional['Comm'] = None, - title: Optional[str]=None, notebook: bool = False, location: bool | Location = True + self, doc: Document | None = None, comm: Comm | None = None, + title: str | None=None, notebook: bool = False, location: bool | Location = True ) -> Document: title = self.title if self.title != self.param.title.default else title if self.busy_indicator: @@ -782,14 +782,22 @@ def _update_vars(self, *args) -> None: img = _panel(self.logo) if not isinstance(img, ImageBase): raise ValueError(f"Could not determine file type of logo: {self.logo}.") - logo = img._b64(img._data(img.object)) + imgdata = img._data(img.object) + if imgdata: + logo = img._b64(imgdata) + else: + raise ValueError(f"Could not embed logo {self.logo}.") else: logo = self.logo if os.path.isfile(self.favicon): img = _panel(self.favicon) if not isinstance(img, ImageBase): raise ValueError(f"Could not determine file type of favicon: {self.favicon}.") - favicon = img._b64(img._data(img.object)) + imgdata = img._data(img.object) + if imgdata: + favicon = img._b64(imgdata) + else: + raise ValueError(f"Could not embed favicon {self.favicon}.") else: if _settings.resources(default='server') == 'cdn' and self.favicon == FAVICON_URL: favicon = CDN_DIST + "images/favicon.ico" @@ -942,7 +950,7 @@ class Template(BaseTemplate): def __init__( self, template: str | _Template, nb_template: str | _Template | None = None, - items: Optional[dict[str, Any]] = None, **params + items: dict[str, Any] | None = None, **params ): super().__init__(template=template, nb_template=nb_template, items=items, **params) items = {} if items is None else items @@ -953,7 +961,7 @@ def __init__( # Public API #---------------------------------------------------------------- - def add_panel(self, name: str, panel: Viewable, tags: list[str] = []) -> None: + def add_panel(self, name: str, panel: Any, tags: list[str] = []) -> None: """ Add panels to the Template, which may then be referenced by the given name using the jinja2 embed macro. @@ -970,7 +978,10 @@ def add_panel(self, name: str, panel: Viewable, tags: list[str] = []) -> None: 'another panel. Ensure each panel ' 'has a unique name by which it can be ' 'referenced in the template.') - self._render_items[name] = (_panel(panel), tags) + rendered = _panel(panel) + if not isinstance(rendered, Viewable): + raise ValueError(f"Cannot add {type(panel).__name__!r} type") + self._render_items[name] = (rendered, tags) self._layout[0].object = repr(self) # type: ignore def add_variable(self, name: str, value: Any) -> None: diff --git a/panel/template/editable/__init__.py b/panel/template/editable/__init__.py index d919e4073a..cf0c512b64 100644 --- a/panel/template/editable/__init__.py +++ b/panel/template/editable/__init__.py @@ -5,7 +5,7 @@ import pathlib -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import TYPE_CHECKING import param @@ -100,7 +100,7 @@ class EditableTemplate(VanillaTemplate): pathlib.Path(__file__).parent / 'editable.css' ] - _resources: ClassVar[dict[str, dict[str, str]]] = { + _resources = { "css": {"lato": "https://fonts.googleapis.com/css?family=Lato&subset=latin,latin-ext"}, "js": { "interactjs": f"{config.npm_cdn}/interactjs@1.10.19/dist/interact.min.js", @@ -123,8 +123,8 @@ def _update_vars(self): super()._update_vars() def _init_doc( - self, doc: Optional[Document] = None, comm: Optional[Comm] = None, - title: Optional[str] = None, notebook: bool = False, + self, doc: Document | None = None, comm: Comm | None = None, + title: str | None = None, notebook: bool = False, location: bool | Location = True ): doc = super()._init_doc(doc, comm, title, notebook, location) diff --git a/panel/template/golden/__init__.py b/panel/template/golden/__init__.py index 20449ab031..e6cf94330d 100644 --- a/panel/template/golden/__init__.py +++ b/panel/template/golden/__init__.py @@ -25,7 +25,7 @@ class GoldenTemplate(BasicTemplate): sidebar_width = param.Integer(default=20, constant=True, doc=""" The width of the sidebar in percent.""") - _css = pathlib.Path(__file__).parent / 'golden.css' + _css = [pathlib.Path(__file__).parent / 'golden.css'] _template = pathlib.Path(__file__).parent / 'golden.html' diff --git a/panel/template/react/__init__.py b/panel/template/react/__init__.py index b0378150d9..0283aec000 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 @@ -37,7 +39,7 @@ class ReactTemplate(BasicTemplate): save_layout = param.Boolean(default=False, doc="Save layout to local storage.") - _css = pathlib.Path(__file__).parent / 'react.css' + _css = [pathlib.Path(__file__).parent / 'react.css'] _template = pathlib.Path(__file__).parent / 'react.html' @@ -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/template/slides/__init__.py b/panel/template/slides/__init__.py index 2737ad9060..5976f099dc 100644 --- a/panel/template/slides/__init__.py +++ b/panel/template/slides/__init__.py @@ -34,7 +34,7 @@ class SlidesTemplate(VanillaTemplate): show_header = param.Boolean(default=False, doc=""" Whether to show the header component.""") - _css = [VanillaTemplate._css, pathlib.Path(__file__).parent / 'slides.css'] + _css = VanillaTemplate._css + [pathlib.Path(__file__).parent / 'slides.css'] _template = pathlib.Path(__file__).parent / 'slides.html' diff --git a/panel/template/vanilla/__init__.py b/panel/template/vanilla/__init__.py index cffc0457ef..d06ab4dd65 100644 --- a/panel/template/vanilla/__init__.py +++ b/panel/template/vanilla/__init__.py @@ -3,8 +3,6 @@ """ import pathlib -from typing import ClassVar - import param from ...theme import Design @@ -22,9 +20,9 @@ class VanillaTemplate(BasicTemplate): is_instance=False, instantiate=False, doc=""" A Design applies a specific design system to a template.""") - _css = pathlib.Path(__file__).parent / 'vanilla.css' + _css = [pathlib.Path(__file__).parent / 'vanilla.css'] - _resources: ClassVar[dict[str, dict[str, str]]] = { + _resources = { 'css': { 'lato': "https://fonts.googleapis.com/css?family=Lato&subset=latin,latin-ext" } diff --git a/panel/tests/chat/test_langchain.py b/panel/tests/chat/test_langchain.py index 4b9a4b6c4f..b510acb182 100644 --- a/panel/tests/chat/test_langchain.py +++ b/panel/tests/chat/test_langchain.py @@ -1,4 +1,3 @@ - from unittest.mock import MagicMock, patch import pytest diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index ce4e06dd00..bc86bf99ec 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -68,7 +68,7 @@ def internet_available(host="8.8.8.8", port=53, timeout=3): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conn: conn.connect((host, port)) return True - except socket.error: + except OSError: return False def port_open(port): diff --git a/panel/tests/io/test_cache.py b/panel/tests/io/test_cache.py index 9031702b27..d0bb5bf21b 100644 --- a/panel/tests/io/test_cache.py +++ b/panel/tests/io/test_cache.py @@ -199,7 +199,7 @@ def test_module_hash(): # Test caching # ################ -OFFSET = {} +OFFSET: dict[tuple, int] = {} def function_with_args(a, b): global OFFSET diff --git a/panel/tests/io/test_document.py b/panel/tests/io/test_document.py index fafbc5e7a2..b935ad5d6e 100644 --- a/panel/tests/io/test_document.py +++ b/panel/tests/io/test_document.py @@ -1,5 +1,3 @@ - - from panel.io.document import unlocked from panel.io.state import set_curdoc from panel.tests.util import serve_and_request diff --git a/panel/tests/io/test_handlers.py b/panel/tests/io/test_handlers.py index 9b9664300f..608ab2a024 100644 --- a/panel/tests/io/test_handlers.py +++ b/panel/tests/io/test_handlers.py @@ -7,7 +7,7 @@ try: import nbformat except Exception: - nbformat = None + nbformat = None # type: ignore nbformat_available = pytest.mark.skipif(nbformat is None, reason="requires nbformat") diff --git a/panel/tests/manual/models.py b/panel/tests/manual/models.py deleted file mode 100644 index 2726649336..0000000000 --- a/panel/tests/manual/models.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -Script to manually test the panels custom bokeh models. - -- Models are those defined in index.ts - -- Optional installs: - - ipywidgets - - ipywidgets_bokeh - - plotly - - altair - -""" - -from io import StringIO - -import numpy as np -import param - -from bokeh.sampledata.autompg import autompg - -import panel as pn - -_before = list(locals()) - -# Model: ace -ace = pn.widgets.Ace(value="import sys", language="python", height=100) - -# Model: audio -audio = pn.pane.Audio("http://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3", name="Audio") - -# Model: card -_w1 = pn.widgets.TextInput(name="Text:") -_w2 = pn.widgets.FloatSlider(name="Slider") -card = pn.Card(_w1, _w2) - -# Model: comm_manager -# Model: customselect - -# Model: datetime_picker -datetime_picker = pn.widgets.DatetimePicker() -datetime_range_picker = pn.widgets.DatetimeRangePicker() - -# Model: deckgl -_MAPBOX_KEY = "pk.eyJ1IjoicGFuZWxvcmciLCJhIjoiY2s1enA3ejhyMWhmZjNobjM1NXhtbWRrMyJ9.B_frQsAVepGIe-HiOJeqvQ" -_json_spec = { - "initialViewState": { - "bearing": -27.36, - "latitude": 52.2323, - "longitude": -1.415, - "maxZoom": 15, - "minZoom": 5, - "pitch": 40.5, - "zoom": 6, - }, - "layers": [ - { - "@@type": "HexagonLayer", - "autoHighlight": True, - "coverage": 1, - "data": "https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv", - "elevationRange": [0, 3000], - "elevationScale": 50, - "extruded": True, - "getPosition": "@@=[lng, lat]", - "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211", - "pickable": True, - } - ], - "mapStyle": "mapbox://styles/mapbox/dark-v9", - "views": [{"@@type": "MapView", "controller": True}], -} - -deck_gl = pn.pane.DeckGL(_json_spec, mapbox_api_key=_MAPBOX_KEY) - -# Model: echarts -_echart = { - "title": {"text": "ECharts entry example"}, - "tooltip": {}, - "legend": {"data": ["Sales"]}, - "xAxis": {"data": ["shirt", "cardign", "chiffon shirt", "pants", "heels", "socks"]}, - "yAxis": {}, - "series": [{"name": "Sales", "type": "bar", "data": [5, 20, 36, 10, 10, 20]}], -} -echart_pane = pn.pane.ECharts(_echart, height=480, width=640) - -# Model: file_downloadm -_sio = StringIO() -autompg.to_csv(_sio) -_sio.seek(0) -file_download = pn.widgets.FileDownload(_sio, embed=True, filename="autompg.csv") - -# Model: html (py: Markup) -html = pn.pane.HTML("

TEST

") - -# Model: ipywidget -try: - import ipywidgets as _ipw - import ipywidgets_bokeh as _ipwb # noqa - - ipywidget = pn.pane.IPyWidget(_ipw.FloatSlider(description="Float")) -except ImportError: - ipywidget = "Need to have ipywidgets and ipywidgets_bokeh installed" - -# Model: json (py: Markup) -json = pn.pane.JSON({"test": 1, "B": ["1", None]}) - -# Model: json_editor -json_editor = pn.widgets.JSONEditor( - value={ - "dict": {"key": "value"}, - "float": 3.14, - "int": 1, - "list": [1, 2, 3], - "string": "A string", - }, - width=500, -) - -# Model: katex -latex1 = pn.pane.LaTeX( - "The LaTeX pane supports two delimiters: $LaTeX$ and \(LaTeX\)", - styles={"font-size": "18pt"}, - width=800, -) - -# Model: location - -# Model: mathjax -latex2 = pn.pane.LaTeX( - "$\sum_{j}{\sum_{i}{a*w_{j, i}}}$", renderer="mathjax", styles={"font-size": "18pt"} -) - -# Model: perspective -_data = {"x": [1, 2, 3], "y": [1, 2, 3]} -perspective = pn.pane.Perspective(_data) - -# Model: player -player = pn.widgets.Player( - name="Player", start=0, end=100, value=32, loop_policy="loop" -) - -# Model: plotly -try: - import plotly.express as _px - - plotly = pn.pane.Plotly( - _px.line({"Day": range(7), "Orders": range(7)}, x="Day", y="Orders") - ) -except ImportError: - plotly = "Need to have plotly installed" - -# Model: progress -progress = pn.indicators.Progress(name="Progress", value=20, width=200, height=20) - -# Model: quill -text_editor = pn.widgets.TextEditor(placeholder="Enter some text", width=500) - -# Model: reactive_html -class _Slideshow(pn.reactive.ReactiveHTML): - - index = param.Integer(default=0) - - _template = '' - - def _img_click(self, event): - self.index += 1 - - -_Slideshow.name = "ReactiveHTML
(not working in html)" -reactive = _Slideshow(width=800, height=300) - -# Model: singleselect -single_select = pn.widgets.Select(value="A", options=list("ABCD")) - -# Model: speech_to_text -# speech_to_text_basic = pn.widgets.SpeechToText(button_type="light") - -# Model: state -# Model: tabs -_w1 = pn.widgets.TextInput(name="Text:") -_w2 = pn.widgets.FloatSlider(name="Slider") -tabs = pn.Tabs(_w1, _w2) - -# Model: tabulator -tabulator = pn.widgets.Tabulator(autompg) - - -# Model: terminal -terminal = pn.widgets.Terminal( - "Welcome to the Panel Terminal!\nI'm based on xterm.js\n\n", - options={"cursorBlink": True}, - height=300, - sizing_mode="stretch_width", -) - -# Model: text_to_speech -# text_to_speech = pn.widgets.TextToSpeech(name="Speech Synthesis") - -# Model: trend -_data = {"x": np.arange(50), "y": np.random.randn(50).cumsum()} -trend = pn.indicators.Trend(name="Price", data=_data, width=200, height=200) - -# Model: vega -try: - import altair as _alt - - _chart = ( - _alt.Chart(autompg) - .mark_circle(size=60) - .encode( - x="hp", y="mpg", color="origin", tooltip=["name", "origin", "hp", "mpg"] - ) - .interactive() - ) - - vega = pn.pane.Vega(_chart) -except ImportError: - vega = "Need to have altair installed" - - -# Model: video -video = pn.pane.Video( - "https://file-examples.com/storage/fe2333f3be630e8e7965da7/2017/04/file_example_MP4_480_1_5MG.mp4", - width=640, - height=360, - loop=True, -) - -# Model: videostream -video_stream = pn.widgets.VideoStream(name="Video Stream") - -# Model: vtk.vtkjs -# Model: vtk.vtkvolume -# Model: vtk.vtkaxes -# Model: vtk.vtksynchronized - - -# Combine and save to html -widgets = [v for k, v in locals().items() if k not in _before and not k.startswith("_")] -names = [getattr(w.__class__, "name", w) for w in widgets] -combined = pn.Column( - *[ - pn.Column( - pn.Row(pn.Column(n, width=300), w), - pn.layout.Divider(), - sizing_mode="stretch_width", - ) - for w, n, in zip(widgets, names) - ] -) - -if __name__ == "__main__": - from bokeh.resources import INLINE - - combined.save("models.html", resources=INLINE) -elif __name__.startswith("bokeh"): - combined.servable() 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/test_models.py b/panel/tests/test_models.py index c6af59eea4..d81589cdf1 100644 --- a/panel/tests/test_models.py +++ b/panel/tests/test_models.py @@ -7,5 +7,5 @@ def test_models_encoding(): model_dir = os.path.join(panel.__path__[0], 'models') for file in os.listdir(model_dir): if file.endswith('.ts'): - with open(os.path.join(model_dir, file), 'r') as f: + with open(os.path.join(model_dir, file)) as f: f.read() diff --git a/panel/tests/test_reactive.py b/panel/tests/test_reactive.py index 9d1bc6f4f9..e04e7abd7f 100644 --- a/panel/tests/test_reactive.py +++ b/panel/tests/test_reactive.py @@ -4,7 +4,6 @@ import unittest.mock from functools import partial -from typing import ClassVar, Mapping import bokeh.core.properties as bp import param @@ -67,7 +66,7 @@ class ReactiveRename(Reactive): a = param.Parameter() - _rename: ClassVar[Mapping[str, str | None]] = {'a': 'b'} + _rename = {'a': 'b'} obj = ReactiveRename() diff --git a/panel/tests/test_server.py b/panel/tests/test_server.py index c9e8161aff..0a4f88018c 100644 --- a/panel/tests/test_server.py +++ b/panel/tests/test_server.py @@ -831,7 +831,7 @@ def loaded(): class CustomBootstrapTemplate(BootstrapTemplate): - _css = './assets/custom.css' + _css = ['./assets/custom.css'] def test_server_template_custom_resources(port): diff --git a/panel/tests/test_viewable.py b/panel/tests/test_viewable.py index 43b83807a9..101a7bdbbd 100644 --- a/panel/tests/test_viewable.py +++ b/panel/tests/test_viewable.py @@ -1,11 +1,9 @@ import param import pytest -import panel.custom # To get the custom Viewable - from panel import config from panel.interact import interactive -from panel.pane import Markdown, Str, panel +from panel.pane import Markdown, Str, panel as panel_fn from panel.param import ParamMethod from panel.viewable import ( Child, Children, Viewable, Viewer, is_viewable_param, @@ -57,11 +55,11 @@ class Test(Viewer): def __panel__(self): return "# Test" - test = panel(Test) + test = panel_fn(Test) assert test.object == "# Test" # Confirm that initialized also work - test = panel(Test()) + test = panel_fn(Test()) assert test.object == "# Test" def test_viewer_wraps_panel(): @@ -88,7 +86,7 @@ def test_viewer_wraps_panel_with_deps(document, comm): def test_viewer_with_deps_resolved_by_panel_func(document, comm): tv = ExampleViewerWithDeps(value="hello") - view = panel(tv) + view = panel_fn(tv) view.get_root(document, comm) @@ -103,7 +101,7 @@ class Example: def __panel__(self): return 42 - panel(Example()) + panel_fn(Example()) @pytest.mark.parametrize('viewable', all_viewables) def test_clone(viewable): @@ -114,7 +112,7 @@ def test_clone(viewable): [(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name')]) def test_clone_with_non_defaults(): - v= Viewable(loading=True) + v = Viewable(loading=True) clone = v.clone() assert ([(k, v) for k, v in sorted(v.param.values().items()) if k not in ('name')] == 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/jupyter_server_test_config.py b/panel/tests/ui/jupyter_server_test_config.py index 987826ebca..9bd4e60e87 100644 --- a/panel/tests/ui/jupyter_server_test_config.py +++ b/panel/tests/ui/jupyter_server_test_config.py @@ -1,3 +1,4 @@ +# type: ignore """Server configuration for integration tests. !! Never use this configuration in production because it opens the server to the world and provide access to JupyterLab diff --git a/panel/tests/ui/layout/test_gridspec.py b/panel/tests/ui/layout/test_gridspec.py index faacf4431e..15c0f0ae9a 100644 --- a/panel/tests/ui/layout/test_gridspec.py +++ b/panel/tests/ui/layout/test_gridspec.py @@ -1,4 +1,3 @@ - import pytest from panel import Column, GridSpec, Spacer diff --git a/panel/tests/ui/pane/test_ipywidget.py b/panel/tests/ui/pane/test_ipywidget.py index 470951e831..b5e9757ad6 100644 --- a/panel/tests/ui/pane/test_ipywidget.py +++ b/panel/tests/ui/pane/test_ipywidget.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest pytest.importorskip("ipywidgets") @@ -15,13 +17,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 f86f1a5cc1..212b84a4f5 100644 --- a/panel/tests/util.py +++ b/panel/tests/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import contextlib import http.server @@ -31,29 +33,29 @@ try: import holoviews as hv - hv_version = Version(hv.__version__) + hv_version: Version | None = Version(hv.__version__) except Exception: - hv, hv_version = None, None -hv_available = pytest.mark.skipif(hv is None or hv_version < Version('1.13.0a23'), + 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") try: 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+)/') @@ -370,7 +372,7 @@ def __init__(self, stream) -> None: ''' self._s = stream - self._q = Queue() + self._q: Queue = Queue() def _populateQueue(stream, queue): ''' diff --git a/panel/theme/base.py b/panel/theme/base.py index f6e2a67447..4d82f31e8a 100644 --- a/panel/theme/base.py +++ b/panel/theme/base.py @@ -57,7 +57,7 @@ class Theme(param.Parameterized): A stylesheet that overrides variables specifically for the Theme subclass. In most cases, this is not necessary.""") - modifiers: ClassVar[dict[Viewable, dict[str, Any]]] = {} + modifiers: ClassVar[dict[type[Viewable], dict[str, Any]]] = {} BOKEH_DARK = dict(_dark_minimal.json) @@ -97,10 +97,10 @@ class Design(param.Parameterized, ResourceComponent): theme = param.ClassSelector(class_=Theme, constant=True) # Defines parameter overrides to apply to each model - modifiers: ClassVar[dict[Viewable, dict[str, Any]]] = {} + modifiers: ClassVar[dict[type[Viewable], dict[str, Any]]] = {} # Defines the resources required to render this theme - _resources: ClassVar[dict[str, dict[str, str]]] = {} + _resources = {} # Declares valid themes for this Design _themes: ClassVar[dict[str, type[Theme]]] = { @@ -108,7 +108,7 @@ class Design(param.Parameterized, ResourceComponent): 'dark': DarkTheme } - _cache = {} + _cache: ClassVar[dict[str, ImportedStyleSheet]] = {} def __init__(self, theme=None, **params): if isinstance(theme, type) and issubclass(theme, Theme): @@ -119,8 +119,8 @@ def __init__(self, theme=None, **params): super().__init__(theme=theme, **params) def _reapply( - self, viewable: Viewable, root: Model, old_models: list[Model] = None, - isolated: bool=True, cache=None, document=None + self, viewable: Viewable, root: Model, old_models: list[Model] | None = None, + isolated: bool = True, cache=None, document: Document | None = None ) -> None: ref = root.ref['id'] seen = set() @@ -139,12 +139,17 @@ def _reapply( def _apply_hooks(self, viewable: Viewable, root: Model, changed: Viewable, old_models=None) -> None: from ..io.state import state - if root.document in state._stylesheets: + if root.document is None: + cache: dict[str, ImportedStyleSheet] = {} + elif root.document in state._stylesheets: cache = state._stylesheets[root.document] else: state._stylesheets[root.document] = cache = {} - with root.document.models.freeze(): - self._reapply(changed, root, old_models, isolated=False, cache=cache, document=root.document) + if root.document: + with root.document.models.freeze(): + self._reapply(changed, root, old_models, isolated=False, cache=cache, document=root.document) + else: + self._reapply(changed, root, old_models, isolated=False, cache=cache) def _wrapper(self, viewable): return viewable @@ -186,14 +191,14 @@ def _resolve_modifiers(cls, vtype, theme, is_server=False): @classmethod def _get_modifiers( - cls, viewable: Viewable, theme: Theme = None, isolated: bool = True + cls, viewable: Viewable, theme: Theme | None = None, isolated: bool = True ): from ..io.resources import ( CDN_DIST, component_resource_path, resolve_custom_path, ) theme_type = type(theme) if isinstance(theme, Theme) else theme is_server = bool(state.curdoc.session_context) if not state._is_pyodide and state.curdoc else False - modifiers, child_modifiers = cls._resolve_modifiers(type(viewable), theme_type, is_server=is_server) + modifiers, child_modifiers = cls._resolve_modifiers(type(viewable), theme_type, is_server=is_server) # type: ignore modifiers = dict(modifiers) if 'stylesheets' in modifiers: if isolated: @@ -215,7 +220,7 @@ def _get_modifiers( return modifiers, child_modifiers @classmethod - def _patch_modifiers(cls, doc, modifiers, cache): + def _patch_modifiers(cls, doc: Document | None, modifiers: dict[str, Any], cache: dict[str, ImportedStyleSheet]): if 'stylesheets' in modifiers: stylesheets = [] for sts in modifiers['stylesheets']: @@ -306,7 +311,7 @@ def _apply_params(cls, viewable, mref, modifiers, document=None): # Public API #---------------------------------------------------------------- - def apply(self, viewable: Viewable, root: Model, isolated: bool=True): + def apply(self, viewable: Viewable, root: Model, isolated: bool = True): """ Applies the Design to a Viewable and all it children. diff --git a/panel/util/__init__.py b/panel/util/__init__.py index b7edd2cba9..4a4ccea2e7 100644 --- a/panel/util/__init__.py +++ b/panel/util/__init__.py @@ -258,7 +258,7 @@ def decode_token(token: str, signed: bool = True) -> dict[str, Any]: signing_input, _ = token.encode('utf-8').rsplit(b".", 1) _, payload_segment = signing_input.split(b".", 1) else: - payload_segment = token + payload_segment = token.encode('ascii') return json.loads(base64url_decode(payload_segment).decode('utf-8')) @@ -377,7 +377,7 @@ def parse_timedelta(time_str: str) -> dt.timedelta | None: return dt.timedelta(**time_params) -def fullpath(path: AnyStr | os.PathLike) -> AnyStr | os.PathLike: +def fullpath(path: AnyStr | os.PathLike) -> AnyStr: """Expanduser and then abspath for a given path """ return os.path.abspath(os.path.expanduser(path)) diff --git a/panel/util/checks.py b/panel/util/checks.py index 139c2598ff..e90a4336b8 100644 --- a/panel/util/checks.py +++ b/panel/util/checks.py @@ -5,7 +5,8 @@ import os import sys -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any import numpy as np import param @@ -27,7 +28,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/util/parameters.py b/panel/util/parameters.py index 7474a91ae8..ba2b9153a3 100644 --- a/panel/util/parameters.py +++ b/panel/util/parameters.py @@ -2,8 +2,9 @@ import inspect +from collections.abc import Iterator from contextlib import contextmanager -from typing import Any, Iterator +from typing import Any import param diff --git a/panel/util/warnings.py b/panel/util/warnings.py index 5284046715..8af8014d7c 100644 --- a/panel/util/warnings.py +++ b/panel/util/warnings.py @@ -77,7 +77,7 @@ def deprecated( if isinstance(remove_version, str): remove_version = Version(remove_version) - if remove_version <= base_version and not (current_version.is_prerelease and current_version.pre[0] != 'rc'): + if remove_version <= base_version and not (current_version.pre and current_version.pre[0] != 'rc'): # This error is mainly for developers to remove the deprecated. raise ValueError( f"{old!r} should have been removed in {remove_version}, current version {current_version}." diff --git a/panel/viewable.py b/panel/viewable.py index d1786f9193..5280efff07 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -21,8 +21,9 @@ import typing import uuid +from collections.abc import Callable, Mapping from typing import ( - IO, TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, + IO, TYPE_CHECKING, Any, ClassVar, ) import param # type: ignore @@ -57,11 +58,12 @@ 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 -_tasks = set() +_tasks: set[asyncio.Task] = set() class Layoutable(param.Parameterized): @@ -70,7 +72,7 @@ class Layoutable(param.Parameterized): for all Panel components with a visual representation. """ - align = Align(default='start', doc=""" + align = Align(default="start", 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.""") @@ -309,7 +311,11 @@ class ServableMixin: """ def _modify_doc( - self, server_id: str, title: str, doc: Document, location: Optional['Location'] + self, + server_id: str | None, + title: str, + doc: Document, + location: Location | bool | None ) -> Document: """ Callback to handle FunctionHandler document creation. @@ -319,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 @@ -331,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: @@ -345,9 +357,9 @@ def _add_location( #---------------------------------------------------------------- def servable( - self, title: Optional[str] = None, location: bool | 'Location' = True, - area: str = 'main', target: Optional[str] = None - ) -> 'ServableMixin': + 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 pn.state.template if in a `panel serve` context, writes to the @@ -374,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): @@ -415,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. @@ -549,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. @@ -588,7 +601,7 @@ def _cleanup(self, root: Model | None = None) -> None: if ref in state._handles: del state._handles[ref] - def _preprocess(self, root: 'Model', changed=None, old_models=None) -> None: + def _preprocess(self, root: Model, changed=None, old_models=None) -> None: """ Applies preprocessing hooks to the root model. @@ -606,7 +619,7 @@ def _preprocess(self, root: 'Model', changed=None, old_models=None) -> None: else: 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: @@ -627,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. """ @@ -646,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: """ @@ -684,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 @@ -698,7 +712,7 @@ class Viewable(Renderable, Layoutable, ServableMixin): Whether or not the Viewable is loading. If True a loading spinner is shown on top of the Viewable.""") - _preprocessing_hooks: ClassVar[list[Callable[['Viewable', 'Model'], None]]] = [] + _preprocessing_hooks: ClassVar[list[Callable[[Viewable, Model], None]]] = [] def __init__(self, **params): hooks = params.pop('hooks', []) @@ -733,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: @@ -842,7 +856,7 @@ def __str__(self) -> str: # Public API #---------------------------------------------------------------- - def clone(self, **params) -> 'Viewable': + def clone(self, **params) -> Viewable: """ Makes a copy of the object sharing the same parameters. @@ -858,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. @@ -883,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 @@ -917,11 +931,11 @@ def embed( ) def save( - self, filename: str | os.PathLike | IO, title: Optional[str] = None, + self, filename: str | os.PathLike | IO, title: str | None = None, resources: Resources | None = None, template: str | Template | None = None, template_variables: dict[str, Any] = {}, embed: bool = False, max_states: int = 1000, max_opts: int = 3, embed_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 = True, embed_states: dict[Any, Any] = {}, as_png: bool | None = None, **kwargs ) -> None: @@ -970,8 +984,8 @@ def save( ) def server_doc( - self, doc: Optional[Document] = None, title: Optional[str] = None, - location: bool | 'Location' = True + self, doc: Document | None = None, title: str | None = None, + location: bool | Location = True ) -> Document: """ Returns a serveable bokeh Document with the panel attached @@ -1017,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 @@ -1053,18 +1071,18 @@ def _create_view(self): return view 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 ) -> Viewable: return self._create_view().servable(title, location, area, target) servable.__doc__ = ServableMixin.servable.__doc__ 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 - ) -> threading.Thread | '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 + ) -> threading.Thread | Server: return self._create_view().show( title, port, address, websocket_origin, threaded, verbose, open, location, **kwargs @@ -1084,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/_mixin.py b/panel/widgets/_mixin.py index 8eb8747aa2..898d29bc73 100644 --- a/panel/widgets/_mixin.py +++ b/panel/widgets/_mixin.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, ClassVar, Mapping +from collections.abc import Mapping +from typing import Any, ClassVar import param diff --git a/panel/widgets/base.py b/panel/widgets/base.py index 0d75ad2465..78fcebc790 100644 --- a/panel/widgets/base.py +++ b/panel/widgets/base.py @@ -7,8 +7,9 @@ import math +from collections.abc import Callable, Mapping from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, TypeVar, + TYPE_CHECKING, Any, ClassVar, TypeVar, ) import param # type: ignore @@ -96,7 +97,7 @@ 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 @@ -115,7 +116,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') @@ -145,9 +146,13 @@ def _process_param_change(self, params: dict[str, Any]) -> dict[str, Any]: return params def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: + if self._widget_type is None: + raise NotImplementedError( + 'Widget {type(self).__name__} did not define a _widget_type' + ) model = self._widget_type(**self._get_properties(doc)) root = root or model self._models[root.ref['id']] = (model, parent) @@ -155,8 +160,8 @@ def _get_model( return model def _get_embed_state( - self, root: 'Model', values: Optional[list[Any]] = None, max_opts: int = 3 - ) -> tuple['Widget', 'Model', list[Any], Callable[['Model'], Any], str, str]: + self, root: Model, values: list[Any] | None = None, max_opts: int = 3 + ) -> tuple[Widget, Model, list[Any], Callable[[Model], Any], str, str]: """ Returns the bokeh model and a discrete set of value states for the widget. @@ -185,6 +190,7 @@ def _get_embed_state( js_getter: string JS snippet that returns the state value given the model """ + raise NotImplementedError() class CompositeWidget(Widget): @@ -195,7 +201,7 @@ class CompositeWidget(Widget): _composite_type: ClassVar[type[ListPanel]] = Row - _linked_properties: ClassVar[tuple[str]] = () + _linked_properties: tuple[str, ...] = () __abstract = True @@ -221,7 +227,7 @@ def _update_layout_params(self, *events: param.parameterized.Event) -> None: self._composite.param.update(**updates) def select( - self, selector: Optional[type | Callable[['Viewable'], bool]] = None + self, selector: type | Callable[[Viewable], bool] | None = None ) -> list[Viewable]: """ Iterates over the Viewable and any potential children in the @@ -247,8 +253,8 @@ def _cleanup(self, root: Model | None = None) -> None: super()._cleanup(root) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = self._composite._get_model(doc, root, parent, comm) root = model if root is None else root diff --git a/panel/widgets/button.py b/panel/widgets/button.py index 344b3841f7..e2cfe14228 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -4,9 +4,8 @@ """ from __future__ import annotations -from typing import ( - TYPE_CHECKING, Any, Awaitable, Callable, ClassVar, Mapping, Optional, -) +from collections.abc import Awaitable, Callable, Mapping +from typing import TYPE_CHECKING, Any, ClassVar import param @@ -73,7 +72,7 @@ class IconMixin(Widget): __abstract = True def __init__(self, **params) -> None: - self._rename = dict(self._rename, **IconMixin._rename) + type(self)._rename = dict(self._rename, **IconMixin._rename) super().__init__(**params) def _process_param_change(self, params): @@ -95,8 +94,8 @@ class _ClickButton(Widget): _event: ClassVar[str] = 'button_click' def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events(self._event, model=model, doc=doc, comm=comm) @@ -200,8 +199,8 @@ def _linkable_params(self) -> list[str]: return super()._linkable_params + ['value'] def jslink( - self, target: JSLinkTarget, code: Optional[dict[str, str]] = None, - args: Optional[dict[str, Any]] = None, bidirectional: bool = False, + self, target: JSLinkTarget, code: dict[str, str] | None = None, + args: dict[str, Any] | None = None, bidirectional: bool = False, **links: str ) -> Link: """ @@ -290,7 +289,7 @@ class Toggle(_ButtonBase, IconMixin): 'value': 'active', 'name': 'label', } - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True _widget_type: ClassVar[type[Model]] = _BkToggle @@ -342,8 +341,8 @@ def __init__(self, **params): self.on_click(click_handler) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events('button_click', model=model, doc=doc, comm=comm) diff --git a/panel/widgets/codeeditor.py b/panel/widgets/codeeditor.py index 7549ad2ce7..307f6763f7 100644 --- a/panel/widgets/codeeditor.py +++ b/panel/widgets/codeeditor.py @@ -3,9 +3,8 @@ """ from __future__ import annotations -from typing import ( - TYPE_CHECKING, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -77,14 +76,13 @@ def _update_value_input(self): self.value_input = self.value def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - if self._widget_type is None: - self._widget_type = lazy_load( - 'panel.models.ace', 'AcePlot', isinstance(comm, JupyterComm), - root, ext='codeeditor' - ) + CodeEditor._widget_type = lazy_load( + 'panel.models.ace', 'AcePlot', isinstance(comm, JupyterComm), + root, ext='codeeditor' + ) return super()._get_model(doc, root, parent, comm) def _update_disabled(self, *events: param.parameterized.Event): diff --git a/panel/widgets/debugger.py b/panel/widgets/debugger.py index 6ff246bf4e..8847dfa632 100644 --- a/panel/widgets/debugger.py +++ b/panel/widgets/debugger.py @@ -6,7 +6,8 @@ import logging -from typing import ClassVar, Mapping +from collections.abc import Mapping +from typing import ClassVar import param @@ -94,9 +95,9 @@ def filter(self,record): if state.curdoc and state.curdoc.session_context: session_id = state.curdoc.session_context.id - widget_session_ids = set(m.document.session_context.id + widget_session_ids = {m.document.session_context.id for m in sum(self.debugger._models.values(), - tuple()) if m.document.session_context) + tuple()) if m.document.session_context} if session_id not in widget_session_ids: return False diff --git a/panel/widgets/file_selector.py b/panel/widgets/file_selector.py index 98fc6158e6..8c1c2c720d 100644 --- a/panel/widgets/file_selector.py +++ b/panel/widgets/file_selector.py @@ -7,7 +7,7 @@ import os from fnmatch import fnmatch -from typing import AnyStr, ClassVar, Optional +from typing import AnyStr, ClassVar import param @@ -137,8 +137,8 @@ def __init__(self, directory: AnyStr | os.PathLike | None = None, **params): self.link(self._selector, size='size') # Set up state - self._stack = [] - self._cwd = None + self._stack: list[str] = [] + self._cwd: str = str(self.directory) self._position = -1 self._update_files(True) @@ -200,10 +200,10 @@ def _refresh(self): self._update_files(refresh=True) def _update_files( - self, event: Optional[param.parameterized.Event] = None, refresh: bool = False + self, event: param.parameterized.Event | None = None, refresh: bool = False ): path = fullpath(self._directory.value) - refresh = refresh or (event and getattr(event, 'obj', None) is self._reload) + refresh = bool(refresh or (event and getattr(event, 'obj', None) is self._reload)) if refresh: path = self._cwd elif not os.path.isdir(path): @@ -263,10 +263,10 @@ def _filter_denylist(self, event: param.parameterized.Event): self._selector.options.update(prefix+[ (k, v) for k, v in options.items() if k in paths or v in self.value ]) - options = [o for o in denylist.options if o in paths] + option_list = [o for o in denylist.options if o in paths] if not self._up.disabled: - options.insert(0, '⬆ ..') - denylist.options = options + option_list.insert(0, '⬆ ..') + denylist.options = option_list def _select(self, event: param.parameterized.Event): if len(event.new) != 1: @@ -293,7 +293,7 @@ def _go_forward(self, event: param.parameterized.Event): self._directory.value = self._stack[self._position] self._update_files() - def _go_up(self, event: Optional[param.parameterized.Event] = None): + def _go_up(self, event: param.parameterized.Event | None = None): path = self._cwd.split(os.path.sep) self._directory.value = os.path.sep.join(path[:-1]) or os.path.sep self._update_files(True) diff --git a/panel/widgets/icon.py b/panel/widgets/icon.py index 16c84d7142..1aaaea9161 100644 --- a/panel/widgets/icon.py +++ b/panel/widgets/icon.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Callable, ClassVar, Mapping +from collections.abc import Callable, Mapping +from typing import ClassVar import param diff --git a/panel/widgets/indicators.py b/panel/widgets/indicators.py index 65c434565a..59d74f9055 100644 --- a/panel/widgets/indicators.py +++ b/panel/widgets/indicators.py @@ -23,10 +23,9 @@ import sys import time +from collections.abc import Mapping from math import pi -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -55,7 +54,7 @@ try: from tqdm.asyncio import tqdm as _tqdm except ImportError: - _tqdm = None + _tqdm = None # type: ignore RED = "#d9534f" GREEN = "#5cb85c" @@ -70,7 +69,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} @@ -135,7 +134,7 @@ async def schedule_off(): def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: events = self._throttle_events(events) if not events: @@ -1222,7 +1221,7 @@ def _process_param_change(self, msg): -class ptqdm(_tqdm or object): +class ptqdm(_tqdm or object): # type: ignore def __init__(self, *args, **kwargs): if _tqdm is None: @@ -1350,8 +1349,8 @@ def __init__(self, **params): self._lock = params.pop('lock', None) def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = self.layout._get_model(doc, root, parent, comm) root = root or model diff --git a/panel/widgets/input.py b/panel/widgets/input.py index b5337e977f..f5d811282c 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -8,9 +8,10 @@ import json from base64 import b64decode +from collections.abc import Iterable, Mapping from datetime import date, datetime, time as dt_time from typing import ( - TYPE_CHECKING, Any, ClassVar, Iterable, Mapping, Optional, + TYPE_CHECKING, Any, ClassVar, Type, ) import numpy as np @@ -111,8 +112,8 @@ class TextInput(_TextInputBase): _rename = {'enter_pressed': None} def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events('enter-pressed', model=model, doc=doc, comm=comm) @@ -255,7 +256,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',) @@ -379,10 +380,10 @@ def __init__(self, **params): self._file_buffer = {} def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - self._widget_type = lazy_load( + FileDropper._widget_type = lazy_load( 'panel.models.file_dropper', 'FileDropper', isinstance(comm, JupyterComm), root, ext='filedropper' ) @@ -408,8 +409,8 @@ def _process_event(self, event: DeleteEvent | UploadEvent): return buffers = self._file_buffer.pop(name) - file_buffer = b''.join(buffers) - if data['type'].startswith('text/'): + file_buffer: bytes | str = b''.join(buffers) + if data['type'].startswith('text/') and isinstance(file_buffer, bytes): try: file_buffer = file_buffer.decode('utf-8') except UnicodeDecodeError: @@ -449,7 +450,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,12 +1005,12 @@ 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( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: if 'value_throttled' in msg: del msg['value_throttled'] @@ -1168,7 +1169,7 @@ class LiteralInput(Widget): 'value': """JSON.stringify(value).replace(/,/g, ",").replace(/:/g, ": ")""" } - _widget_type: ClassVar[type[Model]] = _BkTextInput + _widget_type: ClassVar[Type[Model]] = _BkTextInput # noqa def __init__(self, **params): super().__init__(**params) @@ -1487,7 +1488,7 @@ class _BooleanWidget(Widget): value = param.Boolean(default=False, doc=""" The current value""") - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True _rename: ClassVar[Mapping[str, str | None]] = {'value': 'active', 'name': 'label'} diff --git a/panel/widgets/misc.py b/panel/widgets/misc.py index aea99669a9..39c0534cd2 100644 --- a/panel/widgets/misc.py +++ b/panel/widgets/misc.py @@ -4,8 +4,9 @@ from __future__ import annotations from base64 import b64encode +from collections.abc import Mapping from pathlib import Path -from typing import TYPE_CHECKING, ClassVar, Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -334,9 +335,8 @@ class JSONEditor(Widget): } def _get_model(self, doc, root=None, parent=None, comm=None): - if self._widget_type is None: - self._widget_type = lazy_load( - "panel.models.jsoneditor", "JSONEditor", isinstance(comm, JupyterComm) - ) + JSONEditor._widget_type = lazy_load( + "panel.models.jsoneditor", "JSONEditor", isinstance(comm, JupyterComm) + ) model = super()._get_model(doc, root, parent, comm) return model diff --git a/panel/widgets/player.py b/panel/widgets/player.py index 7f320125c6..6d633d683f 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -3,7 +3,8 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Mapping +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -119,7 +120,7 @@ class Player(PlayerBase): value_throttled = param.Integer(default=0, constant=True, doc=""" Current throttled player value.""") - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True def __init__(self, **params): if 'length' in params: diff --git a/panel/widgets/select.py b/panel/widgets/select.py index bbccae86b5..f2938bcf71 100644 --- a/panel/widgets/select.py +++ b/panel/widgets/select.py @@ -7,11 +7,10 @@ import itertools import re +from collections.abc import Awaitable, Callable, Mapping from functools import partial from types import FunctionType -from typing import ( - TYPE_CHECKING, Any, Awaitable, Callable, ClassVar, Mapping, Optional, -) +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -81,7 +80,7 @@ class SingleSelectBase(SelectBase): _allows_none: ClassVar[bool] = False - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True __abstract = True @@ -276,7 +275,7 @@ def _process_param_change(self, msg: dict[str, Any]) -> dict[str, Any]: groups_provided = 'groups' in msg msg = super()._process_param_change(msg) if groups_provided or 'options' in msg and self.groups: - groups = self.groups + groups: dict[str, list[str | tuple[str, str]]] = self.groups if (all(isinstance(values, dict) for values in groups.values()) is False and all(isinstance(values, list) for values in groups.values()) is False): raise ValueError( @@ -294,7 +293,7 @@ def _process_param_change(self, msg: dict[str, Any]) -> dict[str, Any]: } else: options = { - group: [str(v) for v in self.groups[group]] + group: [str(v) for v in self.groups[group]] # type: ignore for group in groups.keys() } msg['options'] = options @@ -738,7 +737,7 @@ class _MultiSelectBase(SingleSelectBase): description = param.String(default=None, doc=""" An HTML string describing the function of this component.""") - _supports_embed: ClassVar[bool] = False + _supports_embed: bool = False __abstract = True @@ -798,8 +797,8 @@ def __init__(self, **params): self._dbl__click_handlers = [click_handler] if click_handler else [] def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) self._register_events('dblclick_event', model=model, doc=doc, comm=comm) @@ -1050,7 +1049,7 @@ class RadioButtonGroup(_RadioGroupBase, _ButtonBase, TooltipMixin): 'value': "source.labels[value]", 'button_style': None, 'description': None } - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True _widget_type: ClassVar[type[Model]] = _BkRadioButtonGroup @@ -1078,7 +1077,7 @@ class RadioBoxGroup(_RadioGroupBase): Whether the items be arrange vertically (``False``) or horizontally in-line (``True``).""") - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True _widget_type: ClassVar[type[Model]] = _BkRadioBoxGroup diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 305ad5f0fa..f5e77c8694 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -9,9 +9,8 @@ import datetime as dt -from typing import ( - TYPE_CHECKING, Any, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, ClassVar import numpy as np import param @@ -84,7 +83,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): @@ -97,7 +96,7 @@ def _process_property_change(self, msg): def _update_model( self, events: dict[str, param.parameterized.Event], msg: dict[str, Any], - root: Model, model: Model, doc: Document, comm: Optional[Comm] + root: Model, model: Model, doc: Document, comm: Comm | None ) -> None: if 'value_throttled' in msg: del msg['value_throttled'] @@ -115,7 +114,7 @@ class ContinuousSlider(_SliderBase): format = param.ClassSelector(class_=(str, TickFormatter,), doc=""" A custom format string or Bokeh TickFormatter.""") - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True __abstract = True @@ -387,7 +386,7 @@ class DiscreteSlider(CompositeWidget, _SliderBase): 'value': None, 'value_throttled': None, 'options': None } - _supports_embed: ClassVar[bool] = True + _supports_embed: bool = True _style_params: ClassVar[list[str]] = [ p for p in list(Layoutable.param) if p != 'name' diff --git a/panel/widgets/speech_to_text.py b/panel/widgets/speech_to_text.py index 0bede0a8d3..da396f37ca 100644 --- a/panel/widgets/speech_to_text.py +++ b/panel/widgets/speech_to_text.py @@ -20,7 +20,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Mapping +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 9663346b11..7a62e441f6 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -3,10 +3,11 @@ import datetime as dt import uuid +from collections.abc import Callable, Mapping from functools import partial from types import FunctionType, MethodType from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Optional, + TYPE_CHECKING, Any, ClassVar, Literal, Sequence, TypedDict, cast, ) import numpy as np @@ -46,6 +47,35 @@ from ..models.tabulator import ( CellClickEvent, SelectionEvent, TableEditEvent, ) + from ..reactive import TDataColumn + + class FilterSpec(TypedDict, total=False): + headerFilter: str | bool + headerFilterParams: dict[str, Any] + headerFilterFunc: str + headerFilterPlaceholder: str + +class ColumnSpec(TypedDict, total=False): + editable: bool + editor: str | CellEditor + editorParams: dict[str, Any] + field: str + frozen: bool + headerHozAlign: Literal["center", "left", "right"] + headerSort: bool + headerTooltip: str + hozAlign: Literal["center", "left", "right"] + formatter: str | CellFormatter + formatterParams: dict[str, Any] + sorter: str + title: str + titleFormatter: str | CellFormatter + titleFormatterParams: dict[str, Any] + width: str | int + +class GroupSpec(TypedDict): + columns: Sequence[ColumnSpec] + title: str def _convert_datetime_array_ignore_list(v): @@ -194,7 +224,7 @@ def _get_column_definitions(self, col_names: list[str], df: pd.DataFrame) -> lis columns = [] for col in col_names: if col in df.columns: - data = df[col] + data: pd.Series | pd.Index = df[col] elif col in self.indexes: if len(self.indexes) == 1: data = df.index @@ -250,12 +280,12 @@ def _get_column_definitions(self, col_names: list[str], df: pd.DataFrame) -> lis else: if isinstance(formatter, CellFormatter): formatter = clone_model(formatter) - if hasattr(formatter, 'text_align'): + if formatter and hasattr(formatter, 'text_align'): default_text_align = type(formatter).text_align.class_default(formatter) == formatter.text_align else: default_text_align = True - if not hasattr(formatter, 'text_align'): + if not formatter or not hasattr(formatter, 'text_align'): pass elif isinstance(self.text_align, str): formatter.text_align = self.text_align @@ -332,9 +362,10 @@ def _update_cds(self, *events: param.parameterized.Event): self._processed, data = self._get_data() self._update_index_mapping() self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()} + named_events = {e.name: e for e in events} msg = {'data': self._data} for ref, (m, _) in self._models.copy().items(): - self._apply_update(events, msg, m.source, ref) + self._apply_update(named_events, msg, m.source, ref) def _process_param_change(self, params): if 'disabled' in params: @@ -342,7 +373,7 @@ def _process_param_change(self, params): params = super()._process_param_change(params) return params - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None = None) -> dict[str, Any]: properties = super()._get_properties(doc) properties['columns'] = self._get_columns() properties['source'] = cds = ColumnDataSource(data=self._data) @@ -350,11 +381,11 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: return properties def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: properties = self._get_properties(doc) - model = self._widget_type(**properties) + model = self._widget_type(**properties) # type: ignore root = root or model self._link_props(model.source, ['data'], doc, root, comm) self._link_props(model.source.selected, ['indices'], doc, root, comm) @@ -370,7 +401,7 @@ def _update_columns(self, event: param.parameterized.Event, model: Model): def _manual_update( self, events: tuple[param.parameterized.Event, ...], model: Model, doc: Document, - root: Model, parent: Optional[Model], comm: Optional[Comm] + root: Model, parent: Model | None, comm: Comm | None ) -> None: for event in events: if event.type == 'triggered' and self._updating: @@ -426,7 +457,12 @@ def tabulator_sorter(col): df_sorted.drop(columns=['_index_'], inplace=True) return df_sorted - def _filter_dataframe(self, df: pd.DataFrame, header_filters: bool = True, internal_filters: bool = True) -> pd.DataFrame: + def _filter_dataframe( + self, + df: pd.DataFrame, + header_filters: bool = True, + internal_filters: bool = True + ) -> pd.DataFrame: """ Filter the DataFrame. @@ -493,7 +529,7 @@ def _filter_dataframe(self, df: pd.DataFrame, header_filters: bool = True, inter df = df[mask] return df - def _get_header_filters(self, df): + def _get_header_filters(self, df: pd.DataFrame) -> list[pd.Series | np.ndarray]: filters = [] for filt in getattr(self, 'filters', []): col_name = filt['field'] @@ -558,7 +594,7 @@ def _get_header_filters(self, df): raise ValueError(f"Filter type {op!r} not recognized.") return filters - def add_filter(self, filter, column=None): + def add_filter(self, filter: Any, column: str | None = None): """ Adds a filter to the table which can be a static value or dynamic parameter based object which will automatically @@ -616,7 +652,7 @@ def remove_filter(self, filter): if filt is not filter] self._update_cds() - def _process_column(self, values): + def _process_column(self, values: TDataColumn): if not isinstance(values, (list, np.ndarray)): return [str(v) for v in values] if isinstance(values, np.ndarray) and values.dtype.kind == "b": @@ -650,7 +686,7 @@ def _process_df_and_convert_to_cds(self, df: pd.DataFrame) -> tuple[pd.DataFrame data = {k: v for k, v in data.items() if k not in indexes} return df, {k if isinstance(k, str) else str(k): self._process_column(v) for k, v in data.items()} - def _update_column(self, column, array): + def _update_column(self, column: str, array: TDataColumn): import pandas as pd self.value[column] = array @@ -1001,7 +1037,7 @@ class DataFrame(BaseTable): } @property - def _widget_type(self) -> type[Model]: + def _widget_type(self) -> type[Model] | None: # type: ignore[override] return DataCube if self.hierarchical else DataTable def _get_columns(self): @@ -1045,7 +1081,7 @@ def _get_aggregators(self, group): expanded_aggs.append(agg(field_=str(col))) return expanded_aggs - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None = None) -> dict[str, Any]: properties = super()._get_properties(doc) if self.hierarchical: properties['target'] = ColumnDataSource(data=dict(row_indices=[], labels=[])) @@ -1719,7 +1755,7 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): if ind in p_range] super()._update_selected(*events, **kwargs) - def _update_column(self, column: str, array: np.ndarray): + def _update_column(self, column: str, array: TDataColumn) -> None: import pandas as pd if self.pagination != 'remote': @@ -1737,7 +1773,7 @@ def _update_column(self, column: str, array: np.ndarray): with pd.option_context('mode.chained_assignment', None): self._processed.loc[index, column] = array - def _map_indexes(self, indexes, existing=[], add=True): + def _map_indexes(self, indexes: list[int], existing: list[int] = [], add: bool = True) -> list[int]: if self.pagination == 'remote': nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows @@ -1763,18 +1799,19 @@ def _update_expanded(self, expanded): def _update_selection(self, indices: list[int] | SelectionEvent): if isinstance(indices, list): selected = True - ilocs = [] + ilocs: list[int] = [] + inds = indices else: selected = indices.selected ilocs = [] if indices.flush else self.selection.copy() - indices = indices.indices + inds = indices.indices - ilocs = self._map_indexes(indices, ilocs, add=selected) + ilocs = self._map_indexes(inds, ilocs, add=selected) if isinstance(self.selectable, int) and not isinstance(self.selectable, bool): ilocs = ilocs[len(ilocs) - self.selectable:] - self.selection = ilocs + self.selection = ilocs # type: ignore - def _get_properties(self, doc: Document) -> dict[str, Any]: + def _get_properties(self, doc: Document | None = None) -> dict[str, Any]: properties = super()._get_properties(doc) properties['configuration'] = self._get_configuration(properties['columns']) properties['cell_styles'] = self._get_style_data() @@ -1812,8 +1849,8 @@ def _process_param_change(self, params): return params def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: Tabulator._widget_type = lazy_load( 'panel.models.tabulator', 'DataTabulator', isinstance(comm, JupyterComm), root @@ -1827,8 +1864,8 @@ def _get_model( self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm) return model - def _get_filter_spec(self, column: TableColumn) -> dict[str, Any]: - fspec = {} + def _get_filter_spec(self, column: TableColumn) -> FilterSpec: + fspec: FilterSpec = {} if not self.header_filters or (isinstance(self.header_filters, dict) and column.field not in self.header_filters): return fspec @@ -1895,10 +1932,10 @@ def _get_filter_spec(self, column: TableColumn) -> dict[str, Any]: fspec['headerFilterPlaceholder'] = filter_placeholder return fspec - def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any]]: + def _config_columns(self, column_objs: list[TableColumn]) -> Sequence[ColumnSpec | GroupSpec]: column_objs = list(column_objs) - groups = {} - columns = [] + groups: dict[str, GroupSpec] = {} + columns: Sequence[ColumnSpec | GroupSpec] = [] selectable = self.selectable if self.row_content: columns.append({ @@ -1939,25 +1976,25 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] for group, group_cols in self.groups.items() } for i, column in enumerate(ordered_columns): - field = column.field + field = str(column.field) index = self._renamed_cols[field] matching_groups = [ group for group, group_cols in grouping.items() if field in group_cols ] - col_dict = dict(field=field) + col_dict: ColumnSpec = dict(field=field) if isinstance(self.sortable, dict): col_dict['headerSort'] = self.sortable.get(field, True) elif not self.sortable: col_dict['headerSort'] = self.sortable if isinstance(self.text_align, str): - col_dict['hozAlign'] = self.text_align + col_dict['hozAlign'] = self.text_align # type: ignore elif field in self.text_align: col_dict['hozAlign'] = self.text_align[field] if isinstance(self.header_align, str): - col_dict['headerHozAlign'] = self.header_align + col_dict['headerHozAlign'] = self.header_align # type: ignore elif field in self.header_align: - col_dict['headerHozAlign'] = self.header_align[field] + col_dict['headerHozAlign'] = self.header_align[field] # type: ignore formatter = self.formatters.get(field) if isinstance(formatter, str): col_dict['formatter'] = formatter @@ -2015,7 +2052,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] if isinstance(index, tuple): if columns: children = columns - last = children[-1] + last = cast(GroupSpec, children[-1]) for group in index[:-1]: if 'title' in last and last['title'] == group: new = False @@ -2026,7 +2063,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] 'columns': [], 'title': group, }) - last = children[-1] + last = cast(GroupSpec, children[-1]) if new: children = last['columns'] children.append(col_dict) @@ -2036,7 +2073,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] if group in groups: groups[group]['columns'].append(col_dict) continue - group_dict = { + group_dict: GroupSpec = { 'title': group, 'columns': [col_dict] } @@ -2046,7 +2083,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any] columns.append(col_dict) return columns - def _get_configuration(self, columns: list[dict[str, Any]]) -> dict[str, Any]: + def _get_configuration(self, columns: list[TableColumn]) -> dict[str, Any]: """ Returns the Tabulator configuration. """ @@ -2131,7 +2168,7 @@ def on_edit(self, callback: Callable[[TableEditEvent], None]): """ self._on_edit_callbacks.append(callback) - def on_click(self, callback: Callable[[CellClickEvent], None], column: Optional[str] = None): + def on_click(self, callback: Callable[[CellClickEvent], None], column: str | None = None): """ Register a callback to be executed when any cell is clicked. The callback is given a CellClickEvent declaring the column diff --git a/panel/widgets/terminal.py b/panel/widgets/terminal.py index 85facf6020..fca01cbd99 100644 --- a/panel/widgets/terminal.py +++ b/panel/widgets/terminal.py @@ -13,7 +13,8 @@ import subprocess import sys -from typing import ClassVar, Mapping +from collections.abc import Mapping +from typing import ClassVar import param @@ -287,10 +288,9 @@ def write(self, __s): return len(self.output) def _get_model(self, doc, root=None, parent=None, comm=None): - if self._widget_type is None: - self._widget_type = lazy_load( - 'panel.models.terminal', 'Terminal', isinstance(comm, JupyterComm), root - ) + Terminal._widget_type = lazy_load( + 'panel.models.terminal', 'Terminal', isinstance(comm, JupyterComm), root + ) model = super()._get_model(doc, root, parent, comm) model.output = self.output self._register_events('keystroke', model=model, doc=doc, comm=comm) diff --git a/panel/widgets/text_to_speech.py b/panel/widgets/text_to_speech.py index de84d43df3..a8bd0dc392 100644 --- a/panel/widgets/text_to_speech.py +++ b/panel/widgets/text_to_speech.py @@ -11,7 +11,8 @@ import uuid -from typing import TYPE_CHECKING, ClassVar, Mapping +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -68,7 +69,7 @@ def group_by_lang(voices): if not voices: return {} - sorted_lang = sorted(list(set(voice.lang for voice in voices))) + sorted_lang = sorted({voice.lang for voice in voices}) result = {lang: [] for lang in sorted_lang} for voice in voices: result[voice.lang].append(voice) diff --git a/panel/widgets/texteditor.py b/panel/widgets/texteditor.py index 12c2e5ae11..0966b86f9c 100644 --- a/panel/widgets/texteditor.py +++ b/panel/widgets/texteditor.py @@ -3,9 +3,8 @@ """ from __future__ import annotations -from typing import ( - TYPE_CHECKING, ClassVar, Mapping, Optional, -) +from collections.abc import Mapping +from typing import TYPE_CHECKING, ClassVar import param @@ -53,12 +52,11 @@ class TextEditor(Widget): } def _get_model( - self, doc: Document, root: Optional[Model] = None, - parent: Optional[Model] = None, comm: Optional[Comm] = None + self, doc: Document, root: Model | None = None, + parent: Model | None = None, comm: Comm | None = None ) -> Model: - if self._widget_type is None: - self._widget_type = lazy_load( - 'panel.models.quill', 'QuillInput', isinstance(comm, JupyterComm), - root, ext='texteditor' - ) + TextEditor._widget_type = lazy_load( + 'panel.models.quill', 'QuillInput', isinstance(comm, JupyterComm), + root, ext='texteditor' + ) return super()._get_model(doc, root, parent, comm) diff --git a/panel/widgets/widget.py b/panel/widgets/widget.py index 30d1fb7404..628d1cde87 100644 --- a/panel/widgets/widget.py +++ b/panel/widgets/widget.py @@ -2,8 +2,8 @@ from collections.abc import Iterable, Mapping from inspect import Parameter -from numbers import Integral, Number, Real -from typing import Any, Optional +from numbers import Integral, Real +from typing import Any empty = Parameter.empty @@ -37,18 +37,19 @@ def get_interact_value(self): def _get_min_max_value( - min: Number, max: Number, value: Optional[Number] = None, step: Optional[Number] = None -) -> tuple[Number, Number, Number]: + minimum: int | float, maximum: int | float, value: int | float | None = None, step: int | float | None = None +) -> tuple[int | float, int | float, int | float]: """Return min, max, value given input values with possible None.""" # Either min and max need to be given, or value needs to be given if value is None: - if min is None or max is None: - raise ValueError(f'unable to infer range, value from: ({min}, {max}, {value})') - diff = max - min - value = min + (diff / 2) + if minimum is None or max is maximum: + raise ValueError(f'unable to infer range, value from: ({minimum}, {maximum}, {value})') + + diff = maximum - minimum + value = minimum + (diff / 2) # Ensure that value has the same type as diff if not isinstance(value, type(diff)): - value = min + (diff // 2) + value = minimum + (diff // 2) else: # value is not None if not isinstance(value, Real): raise TypeError(f'expected a real number, got: {value!r}') @@ -60,25 +61,25 @@ def _get_min_max_value( vrange = (-value, 3*value) else: vrange = (3*value, -value) - if min is None: - min = vrange[0] - if max is None: - max = vrange[1] + if minimum is None: + minimum = vrange[0] + if maximum is None: + maximum = vrange[1] if step is not None: # ensure value is on a step - tick = int((value - min) / step) - value = min + tick * step - if not min <= value <= max: - raise ValueError(f'value must be between min and max (min={min}, value={value}, max={max})') - return min, max, value + tick = int((value - minimum) / step) + value = minimum + tick * step + if not (minimum <= value <= maximum): + raise ValueError(f'value must be between min and max (min={minimum}, value={value}, max={maximum})') + return minimum, maximum, value -def _matches(o: str, pattern: str) -> bool: +def _matches(o: tuple[Any, ...], pattern: tuple[type, ...]) -> bool: """Match a pattern of types in a sequence.""" if not len(o) == len(pattern): return False - comps = zip(o,pattern) - return all(isinstance(obj,kind) for obj,kind in comps) + comps = zip(o, pattern) + return all(isinstance(obj, kind) for obj, kind in comps) class widget(param.ParameterizedFunction): diff --git a/pixi.toml b/pixi.toml index 9cf82be3f5..cc4519d9c8 100644 --- a/pixi.toml +++ b/pixi.toml @@ -17,7 +17,7 @@ test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit- test-ui = ["py312", "test-core", "test", "test-ui"] test-core = ["py313", "test-core", "test-unit-task"] test-minimum = ["py310", "test-core", "test-unit-task", "test-minimum"] -test-type = ["py311", "type"] +test-type = ["py311", "type", "example", "test"] docs = ["py311", "example", "doc"] build = ["py311", "build"] lint = ["py311", "lint"] @@ -181,6 +181,7 @@ depends_on = ["_install-ui"] # ============================================= [feature.type.dependencies] mypy = "*" +pytest = "*" pandas-stubs = "*" types-bleach = "*" types-croniter = "*" diff --git a/pyproject.toml b/pyproject.toml index b7b34d79c3..d29c88d418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -233,13 +233,15 @@ filterwarnings = [ [tool.mypy] namespace_packages = true explicit_package_bases = true +disable_error_code = "attr-defined" mypy_path = "" exclude = [] [[tool.mypy.overrides]] module = [ - "altair.*", + "anywidget.*", "bokeh_django.*", + "bokeh_fastapi.*", "bokeh.*", "cachecontrol.*", "cryptography.*", @@ -247,12 +249,12 @@ module = [ "flask.*", "fsspec.*", "holoviews.*", + "hvplot.*", "ipympl.*", - "ipywidgets_bokeh", + "ipywidgets_bokeh.*", "ipywidgets.*", "js.*", "jupyter_bokeh.*", - "jupyter_server.*", "langchain.*", "lumen.*", "magic.*", @@ -260,25 +262,25 @@ module = [ "mdit_py_emoji.*", "memray.*", "myst_parser.*", - "nbconvert.*", - "nbformat.*", "param.*", "playwright.*", "plotly.*", "pydeck.*", "pyecharts.*", - "pyinstrument.*", "pyodide_http.*", "pyodide.*", + "pyscript.*", + "pyvista.*", "pyviz_comms.*", - "reacton.*", "rpy2.*", "scipy.*", "setuptools_scm.*", "snakeviz.*", "streamz.*", - "textual.*", + "traitlets.*", "tranquilizer.*", + "uvicorn.*", "vtk.*", + "watchfiles.*" ] ignore_missing_imports = true diff --git a/scripts/panelite/generate_panelite_content.py b/scripts/panelite/generate_panelite_content.py index a6251e8cc4..6af41e006a 100644 --- a/scripts/panelite/generate_panelite_content.py +++ b/scripts/panelite/generate_panelite_content.py @@ -19,7 +19,7 @@ BASE_DEPENDENCIES = [] MINIMUM_VERSIONS = {} -INLINE_DIRECTIVE = re.compile('\{.*\}`.*`\s*') +INLINE_DIRECTIVE = re.compile(r'\{.*\}`.*`\s*') # Add piplite command to notebooks with open(DOC_DIR / 'pyodide_dependencies.json', encoding='utf8') as file: