From 090da85f7cd8f7735ad764c76cfea2ae45dab297 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 7 May 2022 20:19:25 +0200 Subject: [PATCH 01/10] add typehints and more --- panel/links.py | 30 ++++++++++++--- panel/reactive.py | 24 ++++++------ panel/widgets/button.py | 82 +++++++++++++++++++++++++++++++---------- 3 files changed, 100 insertions(+), 36 deletions(-) diff --git a/panel/links.py b/panel/links.py index 8c4ceabee7..0b1f9f749a 100644 --- a/panel/links.py +++ b/panel/links.py @@ -4,10 +4,12 @@ import difflib import sys import weakref +from typing import Any, Dict -import param +from bokeh.models import CustomJS, LayoutDOM +from bokeh.models import Model as BkModel -from bokeh.models import CustomJS, Model as BkModel, LayoutDOM +import param from .io.datamodel import create_linked_datamodel from .models import ReactiveHTML @@ -72,7 +74,7 @@ def assert_target_syncable(source, target, properties): "It requires a live Python kernel to have an effect." ) - +UnknownType = Any class Callback(param.Parameterized): """ A Callback defines some callback to be triggered when a property @@ -100,12 +102,29 @@ class Callback(param.Parameterized): # Whether the link requires a target _requires_target = False - def __init__(self, source, target=None, **params): + def __init__(self, source: UnknownType, target: UnknownType=None, args: Dict[str, UnknownType]=None, code: Dict[str, str]=None): + """A Callback defines some callback to be triggered when a property + changes on the source object. A Callback can execute arbitrary + Javascript code and will make all objects referenced in the args + available in the JS namespace. + + Args: + source (UnknownType): The source object + target (UnknownType, optional): _description_. Defaults to None. + args (Dict[str, UnknownType], optional): A `value` object can be referenced by the + `key` name on the JS side. Defaults to None. + code (Dict[str, str], optional): _description_. Defaults to None. + + Raises: + ValueError: _description_ + """ if source is None: raise ValueError('%s must define a source' % type(self).__name__) # Source is stored as a weakref to allow it to be garbage collected self._source = None if source is None else weakref.ref(source) - super().__init__(**params) + if not args: + args={} + super().__init__(args=args, code=code) self.init() def init(self): @@ -163,6 +182,7 @@ def _process_callbacks(cls, root_view, root_model): arg_overrides = {} if 'holoviews' in sys.modules: from holoviews.core.dimension import Dimensioned + from .pane.holoviews import HoloViews, generate_panel_bokeh_map found = [ (link, src, tgt) for (link, src, tgt) in found diff --git a/panel/reactive.py b/panel/reactive.py index 2d8aa14d32..8ee84798c8 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -11,44 +11,44 @@ import re import sys import textwrap - from collections import Counter, defaultdict, namedtuple from functools import partial from pprint import pformat -from typing import ( - TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Mapping, - Optional, Set, Tuple, Type, Union -) +from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List, + Mapping, Optional, Set, Tuple, Type, Union) import bleach import numpy as np -import param # type: ignore - from bokeh.model import DataModel + +import param # type: ignore from param.parameterized import ParameterizedMetaclass, Watcher from .io.document import unlocked from .io.model import hold from .io.notebook import push from .io.state import set_curdoc, state -from .models.reactive_html import ( - DOMEvent, ReactiveHTML as _BkReactiveHTML, ReactiveHTMLParser -) +from .models.reactive_html import DOMEvent +from .models.reactive_html import ReactiveHTML as _BkReactiveHTML +from .models.reactive_html import ReactiveHTMLParser from .util import edit_readonly, escape, updating from .viewable import Layoutable, Renderable, Viewable if TYPE_CHECKING: import pandas as pd - from bokeh.document import Document from bokeh.events import Event from bokeh.model import Model from bokeh.models.sources import DataDict, Patches + from holoviews.core.dimension import Dimensioned from pyviz_comms import Comm from .layout.base import Panel from .links import Callback, Link + # Type Alias + JSLinkTarget=Union[Viewable, Model, 'Dimensioned'] + log = logging.getLogger('panel.reactive') _fields = tuple(Watcher._fields+('target', 'links', 'transformed', 'bidirectional_watcher')) @@ -598,7 +598,7 @@ def jscallback(self, args: Mapping[str, Any]={}, **callbacks: str) -> 'Callback' return Callback(self, code=renamed, args=args) def jslink( - self, target: Any, code: Dict[str, str] = None, args: Optional[Dict] = None, + self, target: 'JSLinkTarget' , code: Dict[str, str] = None, args: Optional[Dict] = None, bidirectional: bool = False, **links: str ) -> 'Link': """ diff --git a/panel/widgets/button.py b/panel/widgets/button.py index 8060306db9..fb2ade0f55 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -3,19 +3,23 @@ events or merely toggling between on-off states. """ from functools import partial +from typing import TYPE_CHECKING, Callable, Dict, List, Optional import param - -from bokeh.models import ( - Button as _BkButton, Toggle as _BkToggle, Dropdown as _BkDropdown -) - -from bokeh.events import MenuItemClick, ButtonClick +from bokeh.events import ButtonClick, MenuItemClick +from bokeh.models import Button as _BkButton +from bokeh.models import Dropdown as _BkDropdown +from bokeh.models import Toggle as _BkToggle +from panel.links import Callback, UnknownType from .base import Widget +if TYPE_CHECKING: + from panel.reactive import JSLinkTarget + + from ..links import Link -BUTTON_TYPES = ['default', 'primary', 'success', 'warning', 'danger','light'] +BUTTON_TYPES: List[str] = ['default', 'primary', 'success', 'warning', 'danger','light'] class _ButtonBase(Widget): @@ -43,7 +47,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None): model.on_event(self._event, partial(self._server_event, doc)) return model - def js_on_click(self, args={}, code=""): + def js_on_click(self, args: Dict[str, UnknownType]={}, code: str="") -> Callback: """ Allows defining a JS callback to be triggered when the button is clicked. @@ -63,9 +67,9 @@ def js_on_click(self, args={}, code=""): from ..links import Callback return Callback(self, code={'event:'+self._event: code}, args=args) - def jscallback(self, args={}, **callbacks): + def jscallback(self, args: Dict[str, UnknownType]={}, **callbacks: str) -> Callback: """ - Allows defining a JS callback to be triggered when a property + Allows defining a Javascript (JS) callback to be triggered when a property changes on the source object. The keyword arguments define the properties that trigger a callback and the JS code that gets executed. @@ -125,17 +129,46 @@ class Button(_ClickButton): def _linkable_params(self): return super()._linkable_params + ['value'] - def jslink(self, target, code=None, args=None, bidirectional=False, **links): + def jslink(self, target: 'JSLinkTarget', code: Dict[str, str]=None, args: Optional[Dict]=None, bidirectional: bool=False, **links: str) -> 'Link': + """ + Links properties on the this Button to those on the + `target` object in Javascript (JS) code. + + Supports two modes, either specify a + mapping between the source and target model properties as + keywords or provide a dictionary of JS code snippets which + maps from the source parameter to a JS code snippet which is + executed when the property changes. + + Arguments + ---------- + target: panel.viewable.Viewable | bokeh.model.Model | holoviews.core.dimension.Dimensioned + The target to link the value(s) to. + code: dict + Custom code which will be executed when the widget value + changes. + args: dict + A mapping of objects to make available to the JS callback + bidirectional: boolean + Whether to link source and target bi-directionally. Default is `False`. + **links: dict[str,str] + A mapping between properties on the source model and the + target model property to link it to. + + Returns + ------- + Link + The Link can be used unlink the widget and the target model. + """ links = {'event:'+self._event if p == 'value' else p: v for p, v in links.items()} super().jslink(target, code, args, bidirectional, **links) - jslink.__doc__ = Widget.jslink.__doc__ - def _process_event(self, event): self.param.trigger('value') self.clicks += 1 - def on_click(self, callback): + def on_click(self, + callback: Callable[[param.parameterized.Event], None]) -> param.parameterized.Watcher: """ Register a callback to be executed when the `Button` is clicked. @@ -143,10 +176,15 @@ def on_click(self, callback): Arguments --------- - callback: (callable) + callback: (Callable[[param.parameterized.Event], None]) The function to run on click events. Must accept a positional `Event` argument + + Returns + ------- + watcher: param.Parameterized.Watcher + A `Watcher` that executes the callback when the button is clicked. """ - self.param.watch(callback, 'clicks', onlychanged=False) + return self.param.watch(callback, 'clicks', onlychanged=False) class Toggle(_ButtonBase): @@ -216,7 +254,8 @@ def _process_event(self, event): item = self.name self.clicked = item - def on_click(self, callback): + def on_click(self, + callback: Callable[[param.parameterized.Event], None]) -> param.parameterized.Watcher: """ Register a callback to be executed when the button is clicked. @@ -224,7 +263,12 @@ def on_click(self, callback): Arguments --------- - callback: (callable) + callback: (Callable[[param.parameterized.Event], None]) The function to run on click events. Must accept a positional `Event` argument + + Returns + ------- + watcher: param.Parameterized.Watcher + A `Watcher` that executes the callback when the MenuButton is clicked. """ - self.param.watch(callback, 'clicked', onlychanged=False) + return self.param.watch(callback, 'clicked', onlychanged=False) From 1ddb3a4b0863cdfa2bed90dc8eea5bd341ef2e3a Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sun, 8 May 2022 19:16:48 +0200 Subject: [PATCH 02/10] add type hints --- panel/util.py | 15 ++++++++------- panel/widgets/file_selector.py | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/panel/util.py b/panel/util.py index 79eff5ca24..35abef744e 100644 --- a/panel/util.py +++ b/panel/util.py @@ -11,19 +11,20 @@ import re import sys import urllib.parse as urlparse - -from collections.abc import MutableSequence, MutableMapping -from collections import defaultdict, OrderedDict +from collections import OrderedDict, defaultdict +from collections.abc import MutableMapping, MutableSequence from contextlib import contextmanager from datetime import datetime from functools import partial -from html import escape # noqa +from html import escape # noqa from importlib import import_module -from packaging.version import Version +from typing import AnyStr, Union import bokeh -import param import numpy as np +from packaging.version import Version + +import param datetime_types = (np.datetime64, dt.datetime, dt.date) @@ -414,7 +415,7 @@ def parse_timedelta(time_str): return dt.timedelta(**time_params) -def fullpath(path): +def fullpath(path: Union[AnyStr, os.PathLike[AnyStr]]) -> Union[AnyStr, os.PathLike[AnyStr]]: """Expanduser and then abspath for a given path """ return os.path.abspath(os.path.expanduser(path)) diff --git a/panel/widgets/file_selector.py b/panel/widgets/file_selector.py index af8eb94458..08617f8c65 100644 --- a/panel/widgets/file_selector.py +++ b/panel/widgets/file_selector.py @@ -3,23 +3,24 @@ directories on the server. """ import os - from collections import OrderedDict from fnmatch import fnmatch +from typing import AnyStr, List, Tuple, Union import param +from _typeshed import StrPath from ..io import PeriodicCallback from ..layout import Column, Divider, Row -from ..viewable import Layoutable from ..util import fullpath +from ..viewable import Layoutable from .base import CompositeWidget from .button import Button from .input import TextInput from .select import CrossSelector -def scan_path(path, file_pattern='*'): +def _scan_path(path: StrPath, file_pattern='*') -> Tuple[List[str], List[str]]: """ Scans the supplied path for files and directories and optionally filters the files with the file keyword, returning a list of sorted @@ -34,7 +35,7 @@ def scan_path(path, file_pattern='*'): Returns ------- - A sorted list of paths + A sorted list of directory paths, A sorted list of files """ paths = [os.path.join(path, p) for p in os.listdir(path)] dirs = [p for p in paths if os.path.isdir(p)] @@ -100,7 +101,7 @@ class FileSelector(CompositeWidget): _composite_type = Column - def __init__(self, directory=None, **params): + def __init__(self, directory: Union[AnyStr, os.PathLike[AnyStr]]=None, **params): from ..pane import Markdown if directory is not None: params['directory'] = fullpath(directory) @@ -207,7 +208,7 @@ def _update_files(self, event=None, refresh=False): self._back.disabled = False selected = self.value - dirs, files = scan_path(path, self.file_pattern) + dirs, files = _scan_path(path, self.file_pattern) for s in selected: check = os.path.realpath(s) if os.path.islink(s) else s if os.path.isdir(check): @@ -228,7 +229,7 @@ def _filter_blacklist(self, event): is not in the current working directory then it is removed from the blacklist. """ - dirs, files = scan_path(self._cwd, self.file_pattern) + dirs, files = _scan_path(self._cwd, self.file_pattern) paths = [('📁' if p in dirs else '')+os.path.relpath(p, self._cwd) for p in dirs+files] blacklist = self._selector._lists[False] options = OrderedDict(self._selector._items) From 846b2ee02b31a2015d098194e4656beb3e6c0ea9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 19:16:08 +0200 Subject: [PATCH 03/10] Fix types --- panel/links.py | 281 +++++++++++++++++++++++++--------------- panel/reactive.py | 6 +- panel/widgets/button.py | 29 +++-- 3 files changed, 199 insertions(+), 117 deletions(-) diff --git a/panel/links.py b/panel/links.py index 0b1f9f749a..73325ca97b 100644 --- a/panel/links.py +++ b/panel/links.py @@ -1,10 +1,16 @@ """ Defines Links which allow declaring links between bokeh properties. """ +from __future__ import annotations + import difflib import sys +import warnings import weakref -from typing import Any, Dict + +from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union +) from bokeh.models import CustomJS, LayoutDOM from bokeh.models import Model as BkModel @@ -16,8 +22,19 @@ from .reactive import Reactive from .viewable import Viewable +if TYPE_CHECKING: + from bokeh.model import Model + + try: + from holoviews.core.dimension import Dimensioned + JSLinkTarget = Union[Reactive, BkModel, 'Dimensioned'] + except Exception: + JSLinkTarget = Union[Reactive, BkModel] # type: ignore + SourceModelSpec = Tuple[Optional[str], str] + TargetModelSpec = Tuple[Optional[str], Optional[str]] -def assert_source_syncable(source, properties): + +def assert_source_syncable(source: 'Reactive', properties: Iterable[str]) -> None: for prop in properties: if prop.startswith('event:'): continue @@ -35,13 +52,13 @@ def assert_source_syncable(source, properties): elif (prop not in source.param and prop not in list(source._rename.values())): matches = difflib.get_close_matches(prop, list(source.param)) if matches: - matches = f' Similar parameters include: {matches!r}' + matches_repr = f' Similar parameters include: {matches!r}' else: - matches = '' + matches_repr = '' raise ValueError( f"Could not jslink {prop!r} parameter (or property) " f"on {type(source).__name__} object because it was not " - "found.{matches}." + f"found. Similar parameters include: {matches_repr}." ) elif (source._source_transforms.get(prop, False) is None or source._rename.get(prop, False) is None): @@ -51,20 +68,22 @@ def assert_source_syncable(source, properties): "to have an effect." ) -def assert_target_syncable(source, target, properties): +def assert_target_syncable( + source: 'Reactive', target: 'JSLinkTarget', properties: Dict[str, str] +) -> None: for k, p in properties.items(): if k.startswith('event:'): continue elif p not in target.param and p not in list(target._rename.values()): matches = difflib.get_close_matches(p, list(target.param)) if matches: - matches = ' Similar parameters include: %r' % matches + matches_repr = ' Similar parameters include: %r' % matches else: - matches = '' + matches_repr = '' raise ValueError( f"Could not jslink {p!r} parameter (or property) " f"on {type(source).__name__} object because it was not " - "found. Similar parameters include: {matches}" + f"found. Similar parameters include: {matches_repr}" ) elif (target._source_transforms.get(p, False) is None or target._rename.get(p, False) is None): @@ -74,7 +93,6 @@ def assert_target_syncable(source, target, properties): "It requires a live Python kernel to have an effect." ) -UnknownType = Any class Callback(param.Parameterized): """ A Callback defines some callback to be triggered when a property @@ -93,30 +111,39 @@ 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() + 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 = {} + _callbacks: Dict[Type['Callback'], Type['CallbackGenerator']] = {} # Whether the link requires a target - _requires_target = False - - def __init__(self, source: UnknownType, target: UnknownType=None, args: Dict[str, UnknownType]=None, code: Dict[str, str]=None): - """A Callback defines some callback to be triggered when a property - changes on the source object. A Callback can execute arbitrary - Javascript code and will make all objects referenced in the args - available in the JS namespace. - - Args: - source (UnknownType): The source object - target (UnknownType, optional): _description_. Defaults to None. - args (Dict[str, UnknownType], optional): A `value` object can be referenced by the - `key` name on the JS side. Defaults to None. - code (Dict[str, str], optional): _description_. Defaults to None. - - Raises: - ValueError: _description_ + _requires_target: bool = False + + def __init__( + self, source: 'Reactive', target: 'JSLinkTarget' = None, + args: Dict[str, Any] = None, code: Dict[str, str] = None, + **params + ): + """ + A Callback defines some callback to be triggered when a + property changes on the source object. A Callback can execute + arbitrary Javascript code and will make all objects referenced + in the args available in the JS namespace. + + Arguments + --------- + source (Reactive): + The source object the callback is attached to. + target (Reactive | Model, optional): + An optional target to trigger some action on when the source + property changes. + args (Dict[str, Any], optional): + Additional args to make available in the Javascript namespace + indexed by name. + code (Dict[str, str], optional): + A dictionary mapping from the changed source property to + a JS code snippet to execute. """ if source is None: raise ValueError('%s must define a source' % type(self).__name__) @@ -124,15 +151,18 @@ def __init__(self, source: UnknownType, target: UnknownType=None, args: Dict[str self._source = None if source is None else weakref.ref(source) if not args: args={} - super().__init__(args=args, code=code) + super().__init__(args=args, code=code, **params) self.init() - def init(self): + def init(self) -> None: """ Registers the Callback """ - if self.source in self.registry: - links = self.registry[self.source] + source = self.source + if source is None: + return + if source in self.registry: + links = self.registry[source] params = { k: v for k, v in self.param.values().items() if k != 'name' } @@ -142,15 +172,15 @@ def init(self): } if not hasattr(link, 'target'): pass - elif (type(link) is type(self) and link.source is self.source + elif (type(link) is type(self) and link.source is source and link.target is self.target and params == link_params): return - self.registry[self.source].append(self) + self.registry[source].append(self) else: - self.registry[self.source] = [self] + self.registry[source] = [self] @classmethod - def register_callback(cls, callback): + def register_callback(cls, callback: Type['CallbackGenerator']) -> None: """ Register a LinkCallback providing the implementation for the Link for a particular backend. @@ -158,16 +188,17 @@ def register_callback(cls, callback): cls._callbacks[cls] = callback @property - def source(self): + def source(self) -> Reactive | None: return self._source() if self._source else None @classmethod - def _process_callbacks(cls, root_view, root_model): + def _process_callbacks(cls, root_view: 'Viewable', root_model: BkModel): if not root_model: return - linkable = root_view.select(Viewable) - linkable += root_model.select({'type' : BkModel}) + linkable = ( + root_view.select(Viewable) + list(root_model.select({'type' : BkModel})) # type: ignore + ) if not linkable: return @@ -179,7 +210,7 @@ def _process_callbacks(cls, root_view, root_model): or isinstance(link.target, param.Parameterized) ] - arg_overrides = {} + arg_overrides: Dict[int, Dict[str, Any]] = {} if 'holoviews' in sys.modules: from holoviews.core.dimension import Dimensioned @@ -206,7 +237,6 @@ def _process_callbacks(cls, root_view, root_model): arg_overrides[id(link)][k] = tgt ref = root_model.ref['id'] - callbacks = [] for (link, src, tgt) in found: cb = cls._callbacks[type(link)] if ((src is None or ref not in getattr(src, '_models', [ref])) or @@ -214,10 +244,7 @@ def _process_callbacks(cls, root_view, root_model): (tgt is not None and ref not in getattr(tgt, '_models', [ref]))): continue overrides = arg_overrides.get(id(link), {}) - callbacks.append( - cb(root_model, link, src, tgt, arg_overrides=overrides) - ) - return callbacks + cb(root_model, link, src, tgt, arg_overrides=overrides) class Link(Callback): @@ -244,7 +271,7 @@ class Link(Callback): # Whether the link requires a target _requires_target = True - def __init__(self, source, target=None, **params): + def __init__(self, source: 'Reactive', target: Optional['JSLinkTarget'] = None, **params): if self._requires_target and target is None: raise ValueError('%s must define a target.' % type(self).__name__) # Source is stored as a weakref to allow it to be garbage collected @@ -252,16 +279,20 @@ def __init__(self, source, target=None, **params): super().__init__(source, **params) @property - def target(self): + def target(self) -> 'JSLinkTarget' | None: return self._target() if self._target else None - def link(self): + def link(self) -> None: """ Registers the Link """ self.init() - if self.source in self.registry: - links = self.registry[self.source] + source = self.source + if source is None: + return + + if source in self.registry: + links = self.registry[source] params = { k: v for k, v in self.param.values().items() if k != 'name' } @@ -269,28 +300,34 @@ def link(self): link_params = { k: v for k, v in link.param.values().items() if k != 'name' } - if (type(link) is type(self) and link.source is self.source + if (type(link) is type(self) and link.source is source and link.target is self.target and params == link_params): return - self.registry[self.source].append(self) + self.registry[source].append(self) else: - self.registry[self.source] = [self] + self.registry[source] = [self] - def unlink(self): + def unlink(self) -> None: """ Unregisters the Link """ - links = self.registry.get(self.source) + source = self.source + if source is None: + return + links = self.registry.get(source, []) if self in links: - links.pop(links.index(self)) + links.remove(self) class CallbackGenerator(object): - error = False + error = True - def __init__(self, root_model, link, source, target=None, arg_overrides={}): + def __init__( + self, root_model: 'Model', link: 'Link', source: 'Reactive', + target: Optional['JSLinkTarget'] = None, arg_overrides: Dict[str, Any] = {} + ): self.root_model = root_model self.link = link self.source = source @@ -310,7 +347,9 @@ def __init__(self, root_model, link, source, target=None, arg_overrides={}): pass @classmethod - def _resolve_model(cls, root_model, obj, model_spec): + def _resolve_model( + cls, root_model: 'Model', obj: 'JSLinkTarget', model_spec: str | None + ) -> 'Model' | None: """ Resolves a model given the supplied object and a model_spec. @@ -353,20 +392,31 @@ def _resolve_model(cls, root_model, obj, model_spec): if model_spec is not None: for spec in model_spec.split('.'): model = getattr(model, spec) + return model - def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, code): + def _init_callback( + self, root_model: 'Model', link: 'Link', source: 'Reactive', + src_spec: 'SourceModelSpec', target: 'JSLinkTarget' | None, + tgt_spec: 'TargetModelSpec', code: Optional[str] + ) -> None: references = {k: v for k, v in link.param.values().items() if k not in ('source', 'target', 'name', 'code', 'args')} src_model = self._resolve_model(root_model, source, src_spec[0]) + if src_model is None: + return ref = root_model.ref['id'] link_id = (id(link), src_spec, tgt_spec) - if (any(link_id in cb.tags for cbs in src_model.js_property_callbacks.values() for cb in cbs) or - any(link_id in cb.tags for cbs in src_model.js_event_callbacks.values() for cb in cbs)): - # Skip registering callback if already registered + callbacks = ( + list(src_model.js_property_callbacks.values()) + # type: ignore + list(src_model.js_event_callbacks.values()) # type: ignore + ) + # Skip registering callback if already registered + if any(link_id in cb.tags for cb in callbacks): return + references['source'] = src_model tgt_model = None @@ -410,12 +460,12 @@ def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, c # Handle links with ReactiveHTML DataModel if isinstance(src_model, ReactiveHTML): - if src_spec[1] in src_model.data.properties(): - references['source'] = src_model = src_model.data + if src_spec[1] in src_model.data.properties(): # type: ignore + references['source'] = src_model = src_model.data # type: ignore if isinstance(tgt_model, ReactiveHTML): - if tgt_spec[1] in tgt_model.data.properties(): - references['target'] = tgt_model = tgt_model.data + if tgt_spec[1] in tgt_model.data.properties(): # type: ignore + references['target'] = tgt_model = tgt_model.data # type: ignore self._initialize_models(link, source, src_model, src_spec[1], target, tgt_model, tgt_spec[1]) self._process_references(references) @@ -432,69 +482,86 @@ def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, c for ev in events: src_model.js_on_event(ev, src_cb) - if getattr(link, 'bidirectional', False): - code = self._get_code(link, target, tgt_spec[1], source, src_spec[1]) - reverse_references = dict(references) - reverse_references['source'] = tgt_model - reverse_references['target'] = src_model - tgt_cb = CustomJS(args=reverse_references, code=code, tags=[link_id]) - changes, events = self._get_triggers(link, tgt_spec) - properties = tgt_model.properties() - for ch in changes: - if ch not in properties: - msg = f"Could not link non-existent property '{ch}' on {tgt_model} model" - if self.error: - raise ValueError(msg) - else: - self.param.warning(msg) - tgt_model.js_on_change(ch, tgt_cb) - for ev in events: - tgt_model.js_on_event(ev, tgt_cb) + tgt_prop = tgt_spec[1] + if not getattr(link, 'bidirectional', False) or tgt_model is None or tgt_prop is None: + return + + code = self._get_code(link, target, tgt_prop, source, src_spec[1]) + reverse_references = dict(references) + reverse_references['source'] = tgt_model + reverse_references['target'] = src_model + tgt_cb = CustomJS(args=reverse_references, code=code, tags=[link_id]) + changes, events = self._get_triggers(link, (None, tgt_prop)) + properties = tgt_model.properties() + for ch in changes: + if ch not in properties: + msg = f"Could not link non-existent property '{ch}' on {tgt_model} model" + if self.error: + raise ValueError(msg) + else: + warnings.warn(msg) + tgt_model.js_on_change(ch, tgt_cb) + for ev in events: + tgt_model.js_on_event(ev, tgt_cb) def _process_references(self, references): """ Method to process references in place. """ - def _get_specs(self, link): + def _get_specs( + 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. """ return [] - def _get_code(self, link, source, target): + def _get_code( + self, link: 'Link', source: 'JSLinkTarget', src_spec: str, + target: 'JSLinkTarget' | None, tgt_spec: str | None + ) -> str: """ Returns the code to be executed. """ return '' - def _get_triggers(self, link, src_spec): + def _get_triggers( + self, link: 'Link', src_spec: 'SourceModelSpec' + ) -> Tuple[List[str], List[str]]: """ Returns the changes and events that trigger the callback. """ return [], [] - def _initialize_models(self, link, source, src_model, src_spec, target, tgt_model, tgt_spec): + def _initialize_models( + 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 models. """ pass - def validate(self): + def validate(self) -> None: pass class JSCallbackGenerator(CallbackGenerator): - def _get_triggers(self, link, src_spec): + def _get_triggers( + 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, source, target): + def _get_specs( + self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget' + ) -> Sequence[Tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: for src_spec, code in link.code.items(): src_specs = src_spec.split('.') if src_spec.startswith('event:'): @@ -575,7 +642,9 @@ class JSLinkCallbackGenerator(JSCallbackGenerator): target['css_classes'] = css_classes """ - def _get_specs(self, link, source, target): + def _get_specs( + self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget' + ) -> Sequence[Tuple['SourceModelSpec', 'TargetModelSpec', str | None]]: if link.code: return super()._get_specs(link, source, target) @@ -600,14 +669,16 @@ def _get_specs(self, link, source, target): specs.append((src_spec, tgt_spec, None)) return specs - def _initialize_models(self, link, source, src_model, src_spec, target, tgt_model, tgt_spec): - if tgt_model and src_spec and tgt_spec: + def _initialize_models( + 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()} src_param = src_reverse.get(src_spec, src_spec) if src_spec.startswith('event:'): return - if (hasattr(source, '_process_property_change') and - src_param in source.param and hasattr(target, '_process_param_change')): + if isinstance(source, Reactive) and src_param in source.param and isinstance(target, Reactive): tgt_reverse = {v: k for k, v in target._rename.items()} tgt_param = tgt_reverse.get(tgt_spec, tgt_spec) value = getattr(source, src_param) @@ -627,7 +698,7 @@ def _initialize_models(self, link, source, src_model, src_spec, target, tgt_mode '%s and no custom code was specified.' % type(self.target).__name__) - def _process_references(self, references): + def _process_references(self, references: Dict[str, str]) -> None: """ Strips target_ prefix from references. """ @@ -636,7 +707,10 @@ def _process_references(self, references): continue references[k[7:]] = references.pop(k) - def _get_code(self, link, source, src_spec, target, tgt_spec): + def _get_code( + 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()} src_param = src_reverse.get(src_spec, src_spec) @@ -648,9 +722,10 @@ def _get_code(self, link, source, src_spec, target, tgt_spec): if isinstance(target, Reactive): tgt_reverse = {v: k for k, v in target._rename.items()} tgt_param = tgt_reverse.get(tgt_spec, tgt_spec) - tgt_transform = target._target_transforms.get(tgt_param) - if tgt_transform is None: + if tgt_param is None: tgt_transform = 'value' + else: + tgt_transform = target._target_transforms.get(tgt_param, 'value') else: tgt_transform = 'value' if tgt_spec == 'loading': diff --git a/panel/reactive.py b/panel/reactive.py index 55a2c7b42f..c616dd244a 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -40,14 +40,10 @@ from bokeh.events import Event from bokeh.model import Model from bokeh.models.sources import DataDict, Patches - from holoviews.core.dimension import Dimensioned from pyviz_comms import Comm from .layout.base import Panel - from .links import Callback, Link - - # Type Alias - JSLinkTarget=Union[Viewable, Model, 'Dimensioned'] + from .links import Callback, JSLinkTarget, Link log = logging.getLogger('panel.reactive') diff --git a/panel/widgets/button.py b/panel/widgets/button.py index fb2ade0f55..9123ced2fd 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -2,15 +2,18 @@ Defines the Button and button-like widgets which allow triggering events or merely toggling between on-off states. """ +from __future__ import annotations + from functools import partial from typing import TYPE_CHECKING, Callable, Dict, List, Optional import param + from bokeh.events import ButtonClick, MenuItemClick from bokeh.models import Button as _BkButton from bokeh.models import Dropdown as _BkDropdown from bokeh.models import Toggle as _BkToggle -from panel.links import Callback, UnknownType +from panel.links import Callback from .base import Widget @@ -19,8 +22,10 @@ from ..links import Link + BUTTON_TYPES: List[str] = ['default', 'primary', 'success', 'warning', 'danger','light'] + class _ButtonBase(Widget): button_type = param.ObjectSelector(default='default', objects=BUTTON_TYPES, doc=""" @@ -47,7 +52,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None): model.on_event(self._event, partial(self._server_event, doc)) return model - def js_on_click(self, args: Dict[str, UnknownType]={}, code: str="") -> Callback: + def js_on_click(self, args: Dict[str, Any] = {}, code: str = "") -> Callback: """ Allows defining a JS callback to be triggered when the button is clicked. @@ -67,7 +72,7 @@ def js_on_click(self, args: Dict[str, UnknownType]={}, code: str="") -> Callback from ..links import Callback return Callback(self, code={'event:'+self._event: code}, args=args) - def jscallback(self, args: Dict[str, UnknownType]={}, **callbacks: str) -> Callback: + def jscallback(self, args: Dict[str, Any] = {}, **callbacks: str) -> Callback: """ Allows defining a Javascript (JS) callback to be triggered when a property changes on the source object. The keyword arguments define the @@ -129,7 +134,11 @@ class Button(_ClickButton): def _linkable_params(self): return super()._linkable_params + ['value'] - def jslink(self, target: 'JSLinkTarget', code: Dict[str, str]=None, args: Optional[Dict]=None, bidirectional: bool=False, **links: str) -> 'Link': + def jslink( + self, target: 'JSLinkTarget', code: Optional[Dict[str, str]] = None, + args: Optional[Dict[str, Any]] = None, bidirectional: bool = False, + **links: str + ) -> 'Link': """ Links properties on the this Button to those on the `target` object in Javascript (JS) code. @@ -163,12 +172,13 @@ def jslink(self, target: 'JSLinkTarget', code: Dict[str, str]=None, args: Option links = {'event:'+self._event if p == 'value' else p: v for p, v in links.items()} super().jslink(target, code, args, bidirectional, **links) - def _process_event(self, event): + def _process_event(self, event: param.parameterized.Event) -> None: self.param.trigger('value') self.clicks += 1 - def on_click(self, - callback: Callable[[param.parameterized.Event], None]) -> param.parameterized.Watcher: + def on_click( + self, callback: Callable[[param.parameterized.Event], None] + ) -> param.parameterized.Watcher: """ Register a callback to be executed when the `Button` is clicked. @@ -254,8 +264,9 @@ def _process_event(self, event): item = self.name self.clicked = item - def on_click(self, - callback: Callable[[param.parameterized.Event], None]) -> param.parameterized.Watcher: + def on_click( + self, callback: Callable[[param.parameterized.Event], None] + ) -> param.parameterized.Watcher: """ Register a callback to be executed when the button is clicked. From b36bf539326fe855f9bf34ad3706469ddabbce79 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 19:40:58 +0200 Subject: [PATCH 04/10] Type util --- panel/util.py | 74 +++++++++++++++++++--------------- panel/widgets/file_selector.py | 4 +- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/panel/util.py b/panel/util.py index 35abef744e..f87aa31044 100644 --- a/panel/util.py +++ b/panel/util.py @@ -1,6 +1,8 @@ """ Various general utilities used in the panel codebase. """ +from __future__ import annotations + import ast import base64 import datetime as dt @@ -11,6 +13,7 @@ import re import sys import urllib.parse as urlparse + from collections import OrderedDict, defaultdict from collections.abc import MutableMapping, MutableSequence from contextlib import contextmanager @@ -18,20 +21,22 @@ from functools import partial from html import escape # noqa from importlib import import_module -from typing import AnyStr, Union +from typing import ( + Any, AnyStr, Dict, Iterable, Iterator, List, Optional, Union +) import bokeh import numpy as np -from packaging.version import Version - import param +from packaging.version import Version + datetime_types = (np.datetime64, dt.datetime, dt.date) bokeh_version = Version(bokeh.__version__) -def isfile(path): +def isfile(path: str) -> bool: """Safe version of os.path.isfile robust to path length issues on Windows""" try: return os.path.isfile(path) @@ -39,7 +44,7 @@ def isfile(path): return False -def isurl(obj, formats=None): +def isurl(obj: Any, formats: Optional[Iterable[str]] = None) -> bool: if not isinstance(obj, str): return False lower_string = obj.lower().split('?')[0].split('#')[0] @@ -49,14 +54,14 @@ def isurl(obj, formats=None): ) and (formats is None or any(lower_string.endswith('.'+fmt) for fmt in formats)) -def is_dataframe(obj): +def is_dataframe(obj) -> bool: if 'pandas' not in sys.modules: return False import pandas as pd return isinstance(obj, pd.DataFrame) -def is_series(obj): +def is_series(obj) -> bool: if 'pandas' not in sys.modules: return False import pandas as pd @@ -104,7 +109,7 @@ def indexOf(obj, objs): raise ValueError('%s not in list' % obj) -def param_name(name): +def param_name(name: str) -> str: """ Removes the integer id from a Parameterized class name. """ @@ -112,7 +117,7 @@ def param_name(name): return name[:name.index(match[0])] if match else name -def recursive_parameterized(parameterized, objects=None): +def recursive_parameterized(parameterized: param.Parameterized, objects=None) -> List[param.Parameterized]: """ Recursively searches a Parameterized object for other Parmeterized objects. @@ -210,7 +215,7 @@ def get_method_owner(meth): return meth.__self__ -def is_parameterized(obj): +def is_parameterized(obj) -> bool: """ Whether an object is a Parameterized class or instance. """ @@ -218,16 +223,18 @@ def is_parameterized(obj): (isinstance(obj, type) and issubclass(obj, param.Parameterized))) -def isdatetime(value): +def isdatetime(value) -> bool: """ Whether the array or scalar is recognized datetime type. """ if is_series(value) and len(value): return isinstance(value.iloc[0], datetime_types) elif isinstance(value, np.ndarray): - return (value.dtype.kind == "M" or - (value.dtype.kind == "O" and len(value) and - isinstance(value[0], datetime_types))) + return bool( + value.dtype.kind == "M" or + (value.dtype.kind == "O" and len(value) and + isinstance(value[0], datetime_types)) + ) elif isinstance(value, list): return all(isinstance(d, datetime_types) for d in value) else: @@ -257,7 +264,7 @@ def datetime_as_utctimestamp(value): return value.replace(tzinfo=dt.timezone.utc).timestamp() * 1000 -def is_number(s): +def is_number(s: Any) -> bool: try: float(s) return True @@ -265,25 +272,28 @@ def is_number(s): return False -def parse_query(query): +def parse_query(query: str) -> Dict[str, Any]: """ Parses a url query string, e.g. ?a=1&b=2.1&c=string, converting numeric strings to int or float types. """ - query = dict(urlparse.parse_qsl(query[1:])) - for k, v in list(query.items()): + query_dict = dict(urlparse.parse_qsl(query[1:])) + parsed_query: Dict[str, Any] = {} + for k, v in query_dict.items(): if v.isdigit(): - query[k] = int(v) + parsed_query[k] = int(v) elif is_number(v): - query[k] = float(v) + parsed_query[k] = float(v) elif v.startswith('[') or v.startswith('{'): try: - query[k] = json.loads(v) + parsed_query[k] = json.loads(v) except Exception: - query[k] = ast.literal_eval(v) + parsed_query[k] = ast.literal_eval(v) elif v.lower() in ("true", "false"): - query[k] = v.lower() == "true" - return query + parsed_query[k] = v.lower() == "true" + else: + parsed_query[k] = v + return parsed_query def base64url_encode(input): @@ -315,7 +325,7 @@ def __get__(self, obj, owner): return self.f(owner) -def url_path(url): +def url_path(url: str) -> str: """ Strips the protocol and domain from a URL returning just the path. """ @@ -326,7 +336,7 @@ def url_path(url): # This functionality should be contributed to param # See https://github.com/holoviz/param/issues/379 @contextmanager -def edit_readonly(parameterized): +def edit_readonly(parameterized: param.Parameterized) -> Iterator: """ Temporarily set parameters on Parameterized object to readonly=False to allow editing them. @@ -390,7 +400,7 @@ def clone_model(bokeh_model, include_defaults=False, include_undefined=False): return type(bokeh_model)(**properties) -def function_name(func): +def function_name(func) -> str: """ Returns the name of a function (or its string repr) """ @@ -403,19 +413,19 @@ def function_name(func): _period_regex = re.compile(r'((?P\d+?)w)?((?P\d+?)d)?((?P\d+?)h)?((?P\d+?)m)?((?P\d+?\.?\d*?)s)?') -def parse_timedelta(time_str): +def parse_timedelta(time_str: str) -> dt.timedelta | None: parts = _period_regex.match(time_str) if not parts: - return - parts = parts.groupdict() + return None + parts_dict = parts.groupdict() time_params = {} - for (name, p) in parts.items(): + for (name, p) in parts_dict.items(): if p: time_params[name] = float(p) return dt.timedelta(**time_params) -def fullpath(path: Union[AnyStr, os.PathLike[AnyStr]]) -> Union[AnyStr, os.PathLike[AnyStr]]: +def fullpath(path: Union[AnyStr, os.PathLike]) -> Union[AnyStr, os.PathLike]: """Expanduser and then abspath for a given path """ return os.path.abspath(os.path.expanduser(path)) diff --git a/panel/widgets/file_selector.py b/panel/widgets/file_selector.py index 08617f8c65..5ebacc55ff 100644 --- a/panel/widgets/file_selector.py +++ b/panel/widgets/file_selector.py @@ -3,12 +3,12 @@ directories on the server. """ import os + from collections import OrderedDict from fnmatch import fnmatch from typing import AnyStr, List, Tuple, Union import param -from _typeshed import StrPath from ..io import PeriodicCallback from ..layout import Column, Divider, Row @@ -20,7 +20,7 @@ from .select import CrossSelector -def _scan_path(path: StrPath, file_pattern='*') -> Tuple[List[str], List[str]]: +def _scan_path(path: str, file_pattern='*') -> Tuple[List[str], List[str]]: """ Scans the supplied path for files and directories and optionally filters the files with the file keyword, returning a list of sorted From 5eb4a48a392f5fb910ffdd5ad6948bfa00c5767f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 19:41:58 +0200 Subject: [PATCH 05/10] Fix type --- panel/widgets/file_selector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/widgets/file_selector.py b/panel/widgets/file_selector.py index 5ebacc55ff..caa3f99872 100644 --- a/panel/widgets/file_selector.py +++ b/panel/widgets/file_selector.py @@ -101,7 +101,7 @@ class FileSelector(CompositeWidget): _composite_type = Column - def __init__(self, directory: Union[AnyStr, os.PathLike[AnyStr]]=None, **params): + def __init__(self, directory: Union[AnyStr, os.PathLike]=None, **params): from ..pane import Markdown if directory is not None: params['directory'] = fullpath(directory) From 012c48650c7870d7aedeb9eeb9f91bc3d8c70927 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 19:46:54 +0200 Subject: [PATCH 06/10] Cleanup --- panel/reactive.py | 18 +++++++++++------- panel/widgets/button.py | 13 ++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/panel/reactive.py b/panel/reactive.py index c616dd244a..5fa258239a 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -11,31 +11,35 @@ import re import sys import textwrap + from collections import Counter, defaultdict, namedtuple from functools import partial from pprint import pformat -from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List, - Mapping, Optional, Set, Tuple, Type, Union) +from typing import ( + TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Mapping, Optional, + Set, Tuple, Type, Union +) import bleach import numpy as np -from bokeh.model import DataModel +import param -import param # type: ignore +from bokeh.model import DataModel from param.parameterized import ParameterizedMetaclass, Watcher from .io.document import unlocked from .io.model import hold from .io.notebook import push from .io.state import set_curdoc, state -from .models.reactive_html import DOMEvent -from .models.reactive_html import ReactiveHTML as _BkReactiveHTML -from .models.reactive_html import ReactiveHTMLParser +from .models.reactive_html import ( + DOMEvent, ReactiveHTML as _BkReactiveHTML, ReactiveHTMLParser +) from .util import edit_readonly, escape, updating from .viewable import Layoutable, Renderable, Viewable if TYPE_CHECKING: import pandas as pd + from bokeh.document import Document from bokeh.events import Event from bokeh.model import Model diff --git a/panel/widgets/button.py b/panel/widgets/button.py index 9123ced2fd..d33a1bc138 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -5,16 +5,16 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Callable, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional import param from bokeh.events import ButtonClick, MenuItemClick -from bokeh.models import Button as _BkButton -from bokeh.models import Dropdown as _BkDropdown -from bokeh.models import Toggle as _BkToggle -from panel.links import Callback +from bokeh.models import ( + Button as _BkButton, Dropdown as _BkDropdown, Toggle as _BkToggle +) +from ..links import Callback from .base import Widget if TYPE_CHECKING: @@ -92,7 +92,6 @@ def jscallback(self, args: Dict[str, Any] = {}, **callbacks: str) -> Callback: callback: Callback The Callback which can be used to disable the callback. """ - from ..links import Callback for k, v in list(callbacks.items()): if k == 'clicks': k = 'event:'+self._event @@ -170,7 +169,7 @@ def jslink( The Link can be used unlink the widget and the target model. """ links = {'event:'+self._event if p == 'value' else p: v for p, v in links.items()} - super().jslink(target, code, args, bidirectional, **links) + return super().jslink(target, code, args, bidirectional, **links) def _process_event(self, event: param.parameterized.Event) -> None: self.param.trigger('value') From e29b25376506f5d035883d5d89c92e94ec885751 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 22:00:44 +0200 Subject: [PATCH 07/10] Add type to panel.layout.base --- panel/layout/base.py | 212 ++++++++++++++++++++++--------------------- panel/links.py | 2 +- panel/reactive.py | 17 ++-- panel/util.py | 4 +- panel/viewable.py | 6 +- 5 files changed, 125 insertions(+), 116 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index 5a9ec6a6ce..6d8ec035c8 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -2,7 +2,13 @@ Defines Layout classes which may be used to arrange panes and widgets in flexible ways to build complex dashboards. """ +from __future__ import annotations + from collections import defaultdict, namedtuple +from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, + Optional, Type +) import param @@ -13,8 +19,15 @@ from ..reactive import Reactive from ..util import param_name, param_reprs -_row = namedtuple("row", ["children"]) -_col = namedtuple("col", ["children"]) +if TYPE_CHECKING: + from bokeh.document import Document + from bokeh.model import Model + from pyviz_comms import Comm + + from ..viewable import Viewable + +_row = namedtuple("row", ["children"]) # type: ignore +_col = namedtuple("col", ["children"]) # type: ignore class Panel(Reactive): @@ -23,23 +36,23 @@ class Panel(Reactive): """ # Used internally to optimize updates - _batch_update = False + _batch_update: bool = False # Bokeh model used to render this Panel - _bokeh_model = None + _bokeh_model: Type['Model'] | None = None # Properties that should sync JS -> Python - _linked_props = [] + _linked_props: List[str] = [] # Parameters which require the preprocessors to be re-run - _preprocess_params = [] + _preprocess_params: List[str] = [] # Parameter -> Bokeh property renaming - _rename = {'objects': 'children'} + _rename: Dict[str, str | None] = {'objects': 'children'} __abstract = True - def __repr__(self, depth=0, max_depth=10): + def __repr__(self, depth: int = 0, max_depth: int = 10) -> str: if depth > max_depth: return '...' spacer = '\n' + (' ' * (depth+1)) @@ -62,7 +75,10 @@ def __repr__(self, depth=0, max_depth=10): # Callback API #---------------------------------------------------------------- - def _update_model(self, events, msg, root, model, doc, 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'] + ) -> None: msg = dict(msg) inverse = {v: k for k, v in self._rename.items() if v is not None} preprocess = any(inverse.get(k, k) in self._preprocess_params for k in msg) @@ -88,7 +104,10 @@ def _update_model(self, events, msg, root, model, doc, comm=None): # Model API #---------------------------------------------------------------- - def _get_objects(self, model, old_objects, doc, root, comm=None): + def _get_objects( + self, model: 'Model', old_objects: List['Viewable'], doc: 'Document', + root: 'Model', comm: Optional['Comm'] = None + ): """ Returns new child models for the layout while reusing unchanged models and cleaning up any dropped objects. @@ -115,7 +134,12 @@ def _get_objects(self, model, old_objects, doc, root, comm=None): new_models.append(child) return new_models - def _get_model(self, doc, root=None, parent=None, comm=None): + def _get_model( + self, doc: Document, root: Optional['Model'] = None, + parent: Optional['Model'] = None, comm: Optional[Comm] = None + ) -> 'Model': + if self._bokeh_model is None: + raise ValueError(f'{type(self).__name__} did not define a _bokeh_model.') model = self._bokeh_model() if root is None: root = model @@ -156,48 +180,40 @@ class ListLike(param.Parameterized): objects = param.List(default=[], doc=""" The list of child objects that make up the layout.""") - _preprocess_params = ['objects'] + _preprocess_params: List[str] = ['objects'] - def __getitem__(self, index): + def __getitem__(self, index: int | slice) -> 'Viewable' | List['Viewable']: return self.objects[index] - def __len__(self): + def __len__(self) -> int: return len(self.objects) - def __iter__(self): + def __iter__(self) -> Iterator['Viewable']: for obj in self.objects: yield obj - def __iadd__(self, other): + def __iadd__(self, other: Iterable[Any]) -> 'ListLike': self.extend(other) return self - def __add__(self, other): + def __add__(self, other: Iterable[Any]) -> 'ListLike': if isinstance(other, ListLike): other = other.objects - if not isinstance(other, list): - stype = type(self).__name__ - otype = type(other).__name__ - raise ValueError("Cannot add items of type %s and %s, can only " - "combine %s.objects with list or ListLike object." - % (stype, otype, stype)) + else: + other = list(other) return self.clone(*(self.objects+other)) - def __radd__(self, other): + def __radd__(self, other: Iterable[Any]) -> 'ListLike': if isinstance(other, ListLike): other = other.objects - if not isinstance(other, list): - stype = type(self).__name__ - otype = type(other).__name__ - raise ValueError("Cannot add items of type %s and %s, can only " - "combine %s.objects with list or ListLike object." - % (otype, stype, stype)) + else: + other = list(other) return self.clone(*(other+self.objects)) - def __contains__(self, obj): + def __contains__(self, obj: 'Viewable') -> bool: return obj in self.objects - def __setitem__(self, index, panes): + def __setitem__(self, index: int | slice, panes: Iterable[Any]) -> None: from ..pane import panel new_objects = list(self) if not isinstance(index, slice): @@ -217,7 +233,7 @@ def __setitem__(self, index, panes): 'got a %s type.' % (type(self).__name__, type(panes).__name__)) expected = len(panes) - new_objects = [None]*expected + new_objects = [None]*expected # type: ignore end = expected elif end > len(self.objects): raise IndexError('Index %d out of bounds on %s ' @@ -234,7 +250,7 @@ def __setitem__(self, index, panes): self.objects = new_objects - def clone(self, *objects, **params): + def clone(self, *objects: Any, **params: Any) -> 'ListLike': """ Makes a copy of the layout sharing the same parameters. @@ -260,7 +276,7 @@ def clone(self, *objects, **params): del p['objects'] return type(self)(*objects, **p) - def append(self, obj): + def append(self, obj: Any) -> None: """ Appends an object to the layout. @@ -273,13 +289,13 @@ def append(self, obj): new_objects.append(panel(obj)) self.objects = new_objects - def clear(self): + def clear(self) -> None: """ Clears the objects on this layout. """ self.objects = [] - def extend(self, objects): + def extend(self, objects: Iterable[Any]) -> None: """ Extends the objects on this layout with a list. @@ -292,7 +308,7 @@ def extend(self, objects): new_objects.extend(list(map(panel, objects))) self.objects = new_objects - def insert(self, index, obj): + def insert(self, index: int, obj: Any) -> None: """ Inserts an object in the layout at the specified index. @@ -306,7 +322,7 @@ def insert(self, index, obj): new_objects.insert(index, panel(obj)) self.objects = new_objects - def pop(self, index): + def pop(self, index: int) -> 'Viewable': """ Pops an item from the layout by index. @@ -315,13 +331,11 @@ def pop(self, index): index (int): The index of the item to pop from the layout. """ new_objects = list(self) - if index in new_objects: - index = new_objects.index(index) obj = new_objects.pop(index) self.objects = new_objects return obj - def remove(self, obj): + def remove(self, obj: 'Viewable') -> None: """ Removes an object from the layout. @@ -333,7 +347,7 @@ def remove(self, obj): new_objects.remove(obj) self.objects = new_objects - def reverse(self): + def reverse(self) -> None: """ Reverses the objects in the layout. """ @@ -382,7 +396,7 @@ def _to_objects_and_names(self, items): names.append(name) return objects, names - def _update_names(self, event): + def _update_names(self, event: param.parameterized.Event) -> None: if len(event.new) == len(self._names): return names = [] @@ -395,56 +409,48 @@ def _update_names(self, event): names.append(name) self._names = names - def _update_active(self, *events): + def _update_active(self, *events: param.parameterized.Event) -> None: pass #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- - def __getitem__(self, index): + def __getitem__(self, index) -> 'Viewable' | List['Viewable']: return self.objects[index] - def __len__(self): + def __len__(self) -> int: return len(self.objects) - def __iter__(self): + def __iter__(self) -> Iterator['Viewable']: for obj in self.objects: yield obj - def __iadd__(self, other): + def __iadd__(self, other: Iterable[Any]) -> 'NamedListLike': self.extend(other) return self - def __add__(self, other): - if isinstance(other, NamedListPanel): - other = list(zip(other._names, other.objects)) + def __add__(self, other: Iterable[Any]) -> 'NamedListLike': + if isinstance(other, NamedListLike): + added = list(zip(other._names, other.objects)) elif isinstance(other, ListLike): - other = other.objects - if not isinstance(other, list): - stype = type(self).__name__ - otype = type(other).__name__ - raise ValueError("Cannot add items of type %s and %s, can only " - "combine %s.objects with list or ListLike object." - % (stype, otype, stype)) + added = other.objects + else: + added = list(other) objects = list(zip(self._names, self.objects)) - return self.clone(*(objects+other)) + return self.clone(*(objects+added)) - def __radd__(self, other): - if isinstance(other, NamedListPanel): - other = list(zip(other._names, other.objects)) + def __radd__(self, other: Iterable[Any]) -> 'NamedListLike': + if isinstance(other, NamedListLike): + added = list(zip(other._names, other.objects)) elif isinstance(other, ListLike): - other = other.objects - if not isinstance(other, list): - stype = type(self).__name__ - otype = type(other).__name__ - raise ValueError("Cannot add items of type %s and %s, can only " - "combine %s.objects with list or ListLike object." - % (otype, stype, stype)) + added = other.objects + else: + added = list(other) objects = list(zip(self._names, self.objects)) - return self.clone(*(other+objects)) + return self.clone(*(added+objects)) - def __setitem__(self, index, panes): + def __setitem__(self, index: int | slice, panes: Iterable[Any]) -> None: new_objects = list(self) if not isinstance(index, slice): if index > len(self.objects): @@ -463,7 +469,7 @@ def __setitem__(self, index, panes): 'got a %s type.' % (type(self).__name__, type(panes).__name__)) expected = len(panes) - new_objects = [None]*expected + new_objects = [None]*expected # type: ignore self._names = [None]*len(panes) end = expected else: @@ -480,7 +486,7 @@ def __setitem__(self, index, panes): new_objects[i], self._names[i] = self._to_object_and_name(pane) self.objects = new_objects - def clone(self, *objects, **params): + def clone(self, *objects: Any, **params: Any) -> 'NamedListLike': """ Makes a copy of the Tabs sharing the same parameters. @@ -493,20 +499,21 @@ def clone(self, *objects, **params): ------- Cloned Tabs object """ - if not objects: - if 'objects' in params: - objects = params.pop('objects') - else: - objects = zip(self._names, self.objects) + if objects: + overrides = objects elif 'objects' in params: raise ValueError('Tabs objects should be supplied either ' 'as positional arguments or as a keyword, ' 'not both.') + elif 'objects' in params: + overrides = params.pop('objects') + else: + overrides = tuple(zip(self._names, self.objects)) p = dict(self.param.values(), **params) del p['objects'] return type(self)(*objects, **params) - def append(self, pane): + def append(self, pane: Any) -> None: """ Appends an object to the tabs. @@ -520,14 +527,14 @@ def append(self, pane): self._names.append(new_name) self.objects = new_objects - def clear(self): + def clear(self) -> None: """ Clears the tabs. """ self._names = [] self.objects = [] - def extend(self, panes): + def extend(self, panes: Iterable[Any]) -> None: """ Extends the the tabs with a list. @@ -541,7 +548,7 @@ def extend(self, panes): self._names.extend(new_names) self.objects = objects - def insert(self, index, pane): + def insert(self, index: int, pane: Any) -> None: """ Inserts an object in the tabs at the specified index. @@ -556,7 +563,7 @@ def insert(self, index, pane): self._names.insert(index, new_name) self.objects = new_objects - def pop(self, index): + def pop(self, index: int) -> 'Viewable': """ Pops an item from the tabs by index. @@ -565,13 +572,12 @@ def pop(self, index): index (int): The index of the item to pop from the tabs. """ new_objects = list(self) - if index in new_objects: - index = new_objects.index(index) - new_objects.pop(index) + obj = new_objects.pop(index) self._names.pop(index) self.objects = new_objects + return obj - def remove(self, pane): + def remove(self, pane: 'Viewable') -> None: """ Removes an object from the tabs. @@ -586,7 +592,7 @@ def remove(self, pane): self._names.pop(index) self.objects = new_objects - def reverse(self): + def reverse(self) -> None: """ Reverses the tabs. """ @@ -610,11 +616,11 @@ class ListPanel(ListLike, Panel): Whether to add scrollbars if the content overflows the size of the container.""") - _source_transforms = {'scroll': None} + _source_transforms: Mapping[str, str | None] = {'scroll': None} __abstract = True - def __init__(self, *objects, **params): + def __init__(self, *objects: Any, **params: Any): from ..pane import panel if objects: if 'objects' in params: @@ -626,7 +632,7 @@ def __init__(self, *objects, **params): params['objects'] = [panel(pane) for pane in params['objects']] super(Panel, self).__init__(**params) - def _process_param_change(self, params): + def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: scroll = params.pop('scroll', None) css_classes = self.css_classes or [] if scroll: @@ -635,8 +641,8 @@ def _process_param_change(self, params): params['css_classes'] = css_classes return super()._process_param_change(params) - def _cleanup(self, root): - if root.ref['id'] in state._fake_roots: + def _cleanup(self, root: 'Model' | None): + if root is not None and root.ref['id'] in state._fake_roots: state._fake_roots.remove(root.ref['id']) super()._cleanup(root) for p in self.objects: @@ -657,11 +663,11 @@ class NamedListPanel(NamedListLike, Panel): Whether to add scrollbars if the content overflows the size of the container.""") - _source_transforms = {'scroll': None} + _source_transforms: Dict[str, str | None] = {'scroll': None} __abstract = True - def _process_param_change(self, params): + def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: scroll = params.pop('scroll', None) css_classes = self.css_classes or [] if scroll: @@ -670,8 +676,8 @@ def _process_param_change(self, params): params['css_classes'] = css_classes return super()._process_param_change(params) - def _cleanup(self, root): - if root.ref['id'] in state._fake_roots: + def _cleanup(self, root: 'Model' | None) -> None: + if root is not None and root.ref['id'] in state._fake_roots: state._fake_roots.remove(root.ref['id']) super()._cleanup(root) for p in self.objects: @@ -696,7 +702,7 @@ class Row(ListPanel): col_sizing = param.Parameter() - _bokeh_model = BkRow + _bokeh_model: Type['Model'] = BkRow _rename = dict(ListPanel._rename, col_sizing='cols') @@ -719,7 +725,7 @@ class Column(ListPanel): row_sizing = param.Parameter() - _bokeh_model = BkColumn + _bokeh_model: Type['Model'] = BkColumn _rename = dict(ListPanel._rename, row_sizing='rows') @@ -763,16 +769,16 @@ class WidgetBox(ListPanel): _rename = {'objects': 'children', 'horizontal': None} @property - def _bokeh_model(self): + def _bokeh_model(self) -> Type['Model']: # type: ignore return BkRow if self.horizontal else BkColumn @param.depends('disabled', 'objects', watch=True) - def _disable_widgets(self): + def _disable_widgets(self) -> None: for obj in self: if hasattr(obj, 'disabled'): obj.disabled = self.disabled - def __init__(self, *objects, **params): + def __init__(self, *objects: Any, **params: Any): super().__init__(*objects, **params) if self.disabled: self._disable_widgets() diff --git a/panel/links.py b/panel/links.py index 73325ca97b..8774d48e67 100644 --- a/panel/links.py +++ b/panel/links.py @@ -414,7 +414,7 @@ def _init_callback( list(src_model.js_event_callbacks.values()) # type: ignore ) # Skip registering callback if already registered - if any(link_id in cb.tags for cb in callbacks): + if any(link_id in cb.tags for cbs in callbacks for cb in cbs): return references['source'] = src_model diff --git a/panel/reactive.py b/panel/reactive.py index 5fa258239a..44884cc3ff 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -91,8 +91,8 @@ class Syncable(Renderable): _js_transforms: Mapping[str, str] = {} # Transforms from input value to bokeh property value - _source_transforms: Mapping[str, str] = {} - _target_transforms: Mapping[str, str] = {} + _source_transforms: Mapping[str, str | None] = {} + _target_transforms: Mapping[str, str | None] = {} __abstract = True @@ -121,7 +121,7 @@ def __init__(self, **params): # Model API #---------------------------------------------------------------- - def _process_property_change(self, msg: Mapping[str, Any]) -> Dict[str, Any]: + def _process_property_change(self, msg: Dict[str, Any]) -> Dict[str, Any]: """ Transform bokeh model property changes into parameter updates. Should be overridden to provide appropriate mapping between @@ -132,7 +132,7 @@ def _process_property_change(self, msg: Mapping[str, Any]) -> Dict[str, Any]: inverted = {v: k for k, v in self._rename.items()} return {inverted.get(k, k): v for k, v in msg.items()} - def _process_param_change(self, msg: Mapping[str, Any]) -> Dict[str, Any]: + def _process_param_change(self, msg: Dict[str, Any]) -> Dict[str, Any]: """ Transform parameter changes into bokeh model property updates. Should be overridden to provide appropriate mapping between @@ -227,7 +227,7 @@ def _update_manual(self, *events: param.parameterized.Event) -> None: cb() def _apply_update( - self, events: Iterable[param.parameterized.Event], msg: Mapping[str, Any], + self, events: Dict[str, param.parameterized.Event], msg: Mapping[str, Any], model: 'Model', ref: str ) -> None: if ref not in state._views or ref in state._fake_roots: @@ -243,8 +243,8 @@ def _apply_update( doc.add_next_tick_callback(cb) def _update_model( - self, events, msg: Mapping[str, Any], root: 'Model', model: 'Model', - doc: 'Document', comm: Optional['Comm'] + self, events: Dict[str, param.parameterized.Event], msg: Dict[str, Any], + root: 'Model', model: 'Model', doc: 'Document', comm: Optional['Comm'] ) -> None: ref = root.ref['id'] self._changing[ref] = attrs = [ @@ -1696,7 +1696,8 @@ def _set_on_model(self, msg: Mapping[str, Any], root: 'Model', model: 'Model') - del self._changing[root.ref['id']] def _update_model( - self, events, msg, root: 'Model', model: 'Model', doc: 'Document', comm: Optional['Comm'] + self, events: Dict[str, param.parameterized.Event], msg: Dict[str, Any], + root: 'Model', model: 'Model', doc: 'Document', comm: Optional['Comm'] ) -> None: child_params = self._parser.children.values() new_children, model_msg, data_msg = {}, {}, {} diff --git a/panel/util.py b/panel/util.py index f87aa31044..2dea24be8c 100644 --- a/panel/util.py +++ b/panel/util.py @@ -230,9 +230,9 @@ def isdatetime(value) -> bool: if is_series(value) and len(value): return isinstance(value.iloc[0], datetime_types) elif isinstance(value, np.ndarray): - return bool( + return ( value.dtype.kind == "M" or - (value.dtype.kind == "O" and len(value) and + (value.dtype.kind == "O" and len(value) != 0 and isinstance(value[0], datetime_types)) ) elif isinstance(value, list): diff --git a/panel/viewable.py b/panel/viewable.py index d9e9c0240f..4d36cee714 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -452,8 +452,10 @@ def __init__(self, **params): 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': + def _get_model( + self, doc: Document, root: Optional['Model'] = None, + parent: Optional['Model'] = None, comm: Optional[Comm] = None + ) -> 'Model': """ Converts the objects being wrapped by the viewable into a bokeh model that can be composed in a bokeh layout. From 472da2cb9e0930780afaf071ea786a1fd9b0dfe1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 22:12:52 +0200 Subject: [PATCH 08/10] Fix flakes --- panel/layout/base.py | 2 +- panel/reactive.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index 6d8ec035c8..f072a96ddd 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -511,7 +511,7 @@ def clone(self, *objects: Any, **params: Any) -> 'NamedListLike': overrides = tuple(zip(self._names, self.objects)) p = dict(self.param.values(), **params) del p['objects'] - return type(self)(*objects, **params) + return type(self)(*overrides, **params) def append(self, pane: Any) -> None: """ diff --git a/panel/reactive.py b/panel/reactive.py index 44884cc3ff..acd32a4e65 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -16,7 +16,7 @@ from functools import partial from pprint import pformat from typing import ( - TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Mapping, Optional, + TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Set, Tuple, Type, Union ) From e9d2f5e40c2c315af96db19a946631f6b3079ee4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 23:18:35 +0200 Subject: [PATCH 09/10] Fix tests --- panel/layout/base.py | 2 +- panel/param.py | 8 ++++---- panel/tests/layout/test_base.py | 2 +- panel/tests/test_viewable.py | 5 ++++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index f072a96ddd..fb0edee7e5 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -363,7 +363,7 @@ class NamedListLike(param.Parameterized): _preprocess_params = ['objects'] - def __init__(self, *items, **params): + def __init__(self, *items: List[Any, Tuple[str, Any]], **params: Any): if 'objects' in params: if items: raise ValueError('%s objects should be supplied either ' diff --git a/panel/param.py b/panel/param.py index 228ad1cbe9..64877d385b 100644 --- a/panel/param.py +++ b/panel/param.py @@ -364,7 +364,7 @@ def update_pane(change, parameter=pname): **kwargs) layout[layout.objects.index(existing[0])] = pane else: - layout.pop(existing[0]) + layout.remove(existing[0]) watchers = [selector.param.watch(update_pane, 'value')] if toggle: @@ -495,7 +495,7 @@ def link(change, watchers=[watcher]): self._rerender() elif (change.new < self.display_threshold and widget in self._widget_box.objects): - self._widget_box.pop(widget) + self._widget_box.remove(widget) elif change.new >= self.display_threshold: self._rerender() return @@ -824,8 +824,8 @@ def update_pane(*events): p.name in w.parameter_names): obj = p.cls if p.inst is None else p.inst obj.param.unwatch(w) - watchers.pop(watchers.index(w)) - deps.pop(deps.index(p)) + watchers.remove(w) + deps.remove(p) new_deps = [dep for dep in new_deps if dep not in deps] for _, params in full_groupby(new_deps, lambda x: (x.inst or x.cls, x.what)): diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index cbd392dc8d..59da8125e5 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -114,7 +114,7 @@ def test_layout_add_error(panel, document, comm): div2 = Div() layout = panel(div1, div2) - with pytest.raises(ValueError): + with pytest.raises(TypeError): layout + 1 diff --git a/panel/tests/test_viewable.py b/panel/tests/test_viewable.py index 75c6380fb5..14d192eb11 100644 --- a/panel/tests/test_viewable.py +++ b/panel/tests/test_viewable.py @@ -25,7 +25,10 @@ def test_viewable_signature(viewable): from inspect import Parameter, signature parameters = signature(viewable).parameters assert 'params' in parameters - assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD) + try: + assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD, annotation='Any') + except: + assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD) def test_Viewer_not_initialized(): From 9f7a7be064af0793fbf161442999107014606862 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 May 2022 23:31:21 +0200 Subject: [PATCH 10/10] Fix flakes --- panel/layout/base.py | 2 +- panel/tests/test_viewable.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index fb0edee7e5..ad534ba812 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -7,7 +7,7 @@ from collections import defaultdict, namedtuple from typing import ( TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, - Optional, Type + Optional, Tuple, Type ) import param diff --git a/panel/tests/test_viewable.py b/panel/tests/test_viewable.py index 14d192eb11..470093ab8d 100644 --- a/panel/tests/test_viewable.py +++ b/panel/tests/test_viewable.py @@ -27,7 +27,7 @@ def test_viewable_signature(viewable): assert 'params' in parameters try: assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD, annotation='Any') - except: + except Exception: assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD)