Skip to content

Commit

Permalink
Add more type hints (#3499)
Browse files Browse the repository at this point in the history
Co-authored-by: Marc Skov Madsen <masma@orsted.dk>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
  • Loading branch information
3 people committed May 16, 2022
1 parent 64a3d49 commit bf747b0
Show file tree
Hide file tree
Showing 10 changed files with 447 additions and 274 deletions.
216 changes: 111 additions & 105 deletions panel/layout/base.py

Large diffs are not rendered by default.

273 changes: 184 additions & 89 deletions panel/links.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)):
Expand Down
27 changes: 14 additions & 13 deletions panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
from functools import partial
from pprint import pformat
from typing import (
TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Mapping,
Optional, Set, Tuple, Type, Union
TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional,
Set, Tuple, Type, Union
)

import bleach
import numpy as np
import param # type: ignore
import param

from bokeh.core.property.descriptors import UnsetValueError
from bokeh.model import DataModel
Expand All @@ -48,7 +48,7 @@
from pyviz_comms import Comm

from .layout.base import Panel
from .links import Callback, Link
from .links import Callback, JSLinkTarget, Link

log = logging.getLogger('panel.reactive')

Expand Down Expand Up @@ -92,8 +92,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

Expand Down Expand Up @@ -122,7 +122,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
Expand All @@ -133,7 +133,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
Expand Down Expand Up @@ -228,7 +228,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:
Expand All @@ -244,8 +244,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 = []
Expand Down Expand Up @@ -607,7 +607,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':
"""
Expand Down Expand Up @@ -1703,7 +1703,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 = {}, {}, {}
Expand Down
2 changes: 1 addition & 1 deletion panel/tests/layout/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
5 changes: 4 additions & 1 deletion panel/tests/test_viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 Exception:
assert parameters['params'] == Parameter('params', Parameter.VAR_KEYWORD)


def test_Viewer_not_initialized():
Expand Down
79 changes: 45 additions & 34 deletions panel/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Various general utilities used in the panel codebase.
"""
from __future__ import annotations

import ast
import base64
import datetime as dt
Expand All @@ -12,33 +14,37 @@
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 (
Any, AnyStr, Dict, Iterable, Iterator, List, Optional, Union
)

import bokeh
import param
import numpy as np
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)
except ValueError: # path too long for Windows
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]
Expand All @@ -48,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
Expand Down Expand Up @@ -103,15 +109,15 @@ 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.
"""
match = re.findall(r'\D+(\d{5,})', 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.
Expand Down Expand Up @@ -209,24 +215,26 @@ 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.
"""
return (isinstance(obj, param.Parameterized) or
(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 (
value.dtype.kind == "M" or
(value.dtype.kind == "O" and len(value) != 0 and
isinstance(value[0], datetime_types))
)
elif isinstance(value, list):
return all(isinstance(d, datetime_types) for d in value)
else:
Expand Down Expand Up @@ -256,33 +264,36 @@ 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
except ValueError:
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):
Expand Down Expand Up @@ -314,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.
"""
Expand All @@ -325,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.
Expand Down Expand Up @@ -389,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)
"""
Expand All @@ -402,19 +413,19 @@ def function_name(func):

_period_regex = re.compile(r'((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\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):
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))
6 changes: 4 additions & 2 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit bf747b0

Please sign in to comment.