diff --git a/param/__init__.py b/param/__init__.py index 99ccc861d..ecd27e33f 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -24,6 +24,8 @@ import re import datetime as dt import collections +import numbers +import typing from .parameterized import ( Parameterized, Parameter, String, ParameterizedFunction, ParamOverrides, @@ -692,7 +694,6 @@ def _force(self,obj,objtype=None): return gen -import numbers def _is_number(obj): if isinstance(obj, numbers.Number): return True # The extra check is for classes that behave like numbers, such as those @@ -752,6 +753,10 @@ def __init__(self, default=b"", regex=None, allow_None=False, **kwargs): self.allow_None = (default is None or allow_None) self._validate(default) + @property + def pytype(self): + return typing.Union[bytes, None] if self.allow_None else bytes + def _validate_regex(self, val, regex): if (val is None and self.allow_None): return @@ -834,6 +839,10 @@ def __init__(self, default=0.0, bounds=None, softbounds=None, self.step = step self._validate(default) + @property + def pytype(self): + return typing.Union[numbers.Number, None] if self.allow_None else numbers.Number + def __get__(self, obj, objtype): """ Same as the superclass's __get__, but if the value was @@ -963,6 +972,10 @@ class Integer(Number): def __init__(self, default=0, **params): Number.__init__(self, default=default, **params) + @property + def pytype(self): + return typing.Union[int, None] if self.allow_None else int + def _validate_value(self, val, allow_None): if callable(val): return @@ -1000,6 +1013,10 @@ def __init__(self, default=False, bounds=(0,1), **params): self.bounds = bounds super(Boolean, self).__init__(default=default, **params) + @property + def pytype(self): + return typing.Union[bool, None] if self.allow_None else bool + def _validate_value(self, val, allow_None): if allow_None: if not isinstance(val, bool) and val is not None: @@ -1034,6 +1051,14 @@ def __init__(self, default=(0,0), length=None, **params): self.length = length self._validate(default) + @property + def pytype(self): + if self.length: + pytype = typing.Tuple[(typing.Any,)*self.length] + else: + ptype = typing.Tuple[typing.Any, ...] + return typing.Union[pytype, None] if self.allow_None else pytype + def _validate_value(self, val, allow_None): if val is None and allow_None: return @@ -1071,6 +1096,14 @@ def deserialize(cls, value): class NumericTuple(Tuple): """A numeric tuple Parameter (e.g. (4.5,7.6,3)) with a fixed tuple length.""" + @property + def pytype(self): + if self.length: + pytype = typing.Tuple[(numbers.Number,)*self.length] + else: + ptype = typing.Tuple[numbers.Number, ...] + return typing.Union[pytype, None] if self.allow_None else pytype + def _validate_value(self, val, allow_None): super(NumericTuple, self)._validate_value(val, allow_None) if allow_None and val is None: @@ -1088,6 +1121,11 @@ class XYCoordinates(NumericTuple): def __init__(self, default=(0.0, 0.0), **params): super(XYCoordinates,self).__init__(default=default, length=2, **params) + @property + def pytype(self): + pytype = typing.Tuple[numbers.Number, numbers.Number] + return typing.Union[pytype, None] if self.allow_None else pytype + class Callable(Parameter): """ @@ -1099,6 +1137,11 @@ class Callable(Parameter): 2.4, so instantiate must be False for those values. """ + @property + def pytype(self): + ctype = typing.Callable[..., typing.Any] + return typing.Union[ctype, None] if self.allow_None else ctype + def _validate_value(self, val, allow_None): if (allow_None and val is None) or callable(val): return @@ -1197,6 +1240,11 @@ class SelectorBase(Parameter): __abstract = True + @property + def pytype(self): + literal = typing.Literal[tuple(self.get_range().values())] + return typing.Union[literal, None] if self.allow_None else literal + def get_range(self): raise NotImplementedError("get_range() must be implemented in subclasses.") @@ -1433,6 +1481,17 @@ def __init__(self, default=[], class_=None, item_type=None, **params) self._validate(default) + @property + def pytype(self): + if isinstance(self.item_type, tuple): + item_type = typing.Union[self.item_type] + elif self.item_type is not None: + item_type = self.item_type + else: + item_type = typing.Any + list_type = typing.List[item_type] + return typing.Union[list_type, None] if self.allow_None else list_type + def _validate(self, val): """ Checks that the value is numeric and that it is within the hard @@ -1487,6 +1546,11 @@ class HookList(List): """ __slots__ = ['class_', 'bounds'] + @property + def pytype(self): + list_type = typing.List[typing.Callable[[], None]] + return typing.Union[list_type, None] if self.allow_None else list_type + def _validate_value(self, val, allow_None): super(HookList, self)._validate_value(val, allow_None) if allow_None and val is None: @@ -1506,6 +1570,12 @@ class Dict(ClassSelector): def __init__(self, default=None, **params): super(Dict, self).__init__(dict, default=default, **params) + @property + def pytype(self): + dict_type = typing.Dict[typing.Hashable, typing.Any] + return typing.Union[dict_type, None] if self.allow_None else dict_type + + class Array(ClassSelector): """ @@ -1513,9 +1583,14 @@ class Array(ClassSelector): """ def __init__(self, default=None, **params): - from numpy import ndarray + from numpy import ndarray super(Array, self).__init__(ndarray, allow_None=True, default=default, **params) + @property + def pytype(self): + from numpy import ndarray + return ndarray + @classmethod def serialize(cls, value): if value is None: @@ -1559,6 +1634,11 @@ def __init__(self, default=None, rows=None, columns=None, ordered=None, **params super(DataFrame,self).__init__(pdDFrame, default=default, **params) self._validate(self.default) + @property + def pytype(self): + from pandas import DataFrame + return DataFrame + def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): @@ -1635,6 +1715,11 @@ def __init__(self, default=None, rows=None, allow_None=False, **params): **params) self._validate(self.default) + @property + def pytype(self): + from pandas import Series + return Series + def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): @@ -1778,6 +1863,13 @@ def __init__(self, default=None, search_paths=None, **params): self.search_paths = search_paths super(Path,self).__init__(default,**params) + @property + def pytype(self): + path_types =(str, pathlib.Path) + if self.allow_None: + path_types += (None,) + return typing.Union[path_types] + def _resolve(self, path): return resolve_path(path, path_to_file=None, search_paths=self.search_paths) @@ -1904,6 +1996,12 @@ def __init__(self, default=None, objects=None, **kwargs): super(ListSelector,self).__init__( objects=objects, default=default, empty_default=True, **kwargs) + @property + def pytype(self): + literal = typing.Literal[tuple(self.get_range().values())] + ltype = typing.List[literal] + return typing.Union[ltype, None] if self.allow_None else ltype + def compute_default(self): if self.default is None and callable(self.compute_default_fn): self.default = self.compute_default_fn() @@ -1954,6 +2052,14 @@ class Date(Number): def __init__(self, default=None, **kwargs): super(Date, self).__init__(default=default, **kwargs) + @property + def pytype(self): + if self.allow_None: + date_types = dt_types + (None,) + else: + date_types = dt_types + return typing.Union[date_types] + def _validate_value(self, val, allow_None): """ Checks that the value is numeric and that it is within the hard @@ -1998,6 +2104,10 @@ class CalendarDate(Number): def __init__(self, default=None, **kwargs): super(CalendarDate, self).__init__(default=default, **kwargs) + @property + def pytype(self): + return typing.Union[dt.datetime, None] if self.allow_None else dt.datetime + def _validate_value(self, val, allow_None): """ Checks that the value is numeric and that it is within the hard @@ -2076,6 +2186,10 @@ def __init__(self, default=None, allow_named=True, **kwargs): self.allow_named = allow_named self._validate(default) + @property + def pytype(self): + return typing.Union[str, None] if self.allow_None else str + def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_allow_named(val, self.allow_named) @@ -2151,6 +2265,12 @@ class DateRange(Range): Bounds must be specified as datetime or date types (see param.dt_types). """ + @property + def pytype(self): + date_type = typing.Union[dt_types] + range_type = typing.Tuple[date_type, date_type] + return typing.Union[range_type, None] if self.allow_None else range_type + def _validate_value(self, val, allow_None): # Cannot use super()._validate_value as DateRange inherits from # NumericTuple which check that the tuple values are numbers and @@ -2205,6 +2325,7 @@ def deserialize(cls, value): # As JSON has no tuple representation return tuple(deserialized) + class CalendarDateRange(Range): """ A date range specified as (start_date, end_date). @@ -2281,6 +2402,10 @@ def __init__(self,default=False,bounds=(0,1),**params): # back to False while triggered callbacks are executing super(Event, self).__init__(default=default,**params) + @property + def pytype(self): + return bool + def _reset_event(self, obj, val): val = False if obj is None: diff --git a/param/parameterized.py b/param/parameterized.py index 607e01f94..ddf6808cd 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -16,6 +16,7 @@ import random import numbers import operator +import typing # Allow this file to be used standalone if desired, albeit without JSON serialization try: @@ -1071,6 +1072,10 @@ class hierarchy (see ParameterizedMetaclass). self.watchers = {} self.per_instance = per_instance + @property + def pytype(self): + return typing.Any + @classmethod def serialize(cls, value): "Given the parameter value, return a Python value suitable for serialization" @@ -1331,6 +1336,10 @@ def __init__(self, default="", regex=None, allow_None=False, **kwargs): self.allow_None = (default is None or allow_None) self._validate(default) + @property + def pytype(self): + return typing.Union[str, None] if self.allow_None else str + def _validate_regex(self, val, regex): if (val is None and self.allow_None): return