1#----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6#----------------------------------------------------------------------------- 7""" Provide base classes for the Bokeh property system. 8 9.. note:: 10 These classes form part of the very low-level machinery that implements 11 the Bokeh model and property system. It is unlikely that any of these 12 classes or their methods will be applicable to any standard usage or to 13 anyone who is not directly developing on Bokeh's own infrastructure. 14 15""" 16 17#----------------------------------------------------------------------------- 18# Boilerplate 19#----------------------------------------------------------------------------- 20import logging # isort:skip 21log = logging.getLogger(__name__) 22 23#----------------------------------------------------------------------------- 24# Imports 25#----------------------------------------------------------------------------- 26 27# Standard library imports 28import types 29from copy import copy 30from typing import Any, Type, Union 31 32# External imports 33import numpy as np 34 35# Bokeh imports 36from ...util.dependencies import import_optional 37from ...util.string import nice_join 38from ..has_props import HasProps 39from .descriptor_factory import PropertyDescriptorFactory 40from .descriptors import BasicPropertyDescriptor 41from .singletons import Intrinsic, Undefined 42 43#----------------------------------------------------------------------------- 44# Globals and constants 45#----------------------------------------------------------------------------- 46 47pd = import_optional('pandas') 48 49#----------------------------------------------------------------------------- 50# General API 51#----------------------------------------------------------------------------- 52 53__all__ = ( 54 'ContainerProperty', 55 'DeserializationError', 56 'PrimitiveProperty', 57 'Property', 58 'validation_on', 59) 60 61#----------------------------------------------------------------------------- 62# Dev API 63#----------------------------------------------------------------------------- 64 65class DeserializationError(Exception): 66 pass 67 68class Property(PropertyDescriptorFactory): 69 """ Base class for Bokeh property instances, which can be added to Bokeh 70 Models. 71 72 Args: 73 default (obj, optional) : 74 A default value for attributes created from this property to have. 75 76 help (str or None, optional) : 77 A documentation string for this property. It will be automatically 78 used by the :ref:`bokeh.sphinxext.bokeh_prop` extension when 79 generating Spinx documentation. (default: None) 80 81 serialized (bool, optional) : 82 Whether attributes created from this property should be included 83 in serialization (default: True) 84 85 readonly (bool, optional) : 86 Whether attributes created from this property are read-only. 87 (default: False) 88 89 """ 90 91 # This class attribute is controlled by external helper API for validation 92 _should_validate = True 93 94 def __init__(self, default=Intrinsic, help=None, serialized=None, readonly=False): 95 default = default if default is not Intrinsic else Undefined 96 97 if serialized is None: 98 self._serialized = False if readonly and default is Undefined else True 99 else: 100 self._serialized = serialized 101 102 self._readonly = readonly 103 self._default = default 104 self._help = help 105 self.__doc__ = help 106 107 self.alternatives = [] 108 self.assertions = [] 109 110 def __str__(self): 111 return self.__class__.__name__ 112 113 @classmethod 114 def _sphinx_prop_link(cls): 115 """ Generate a sphinx :class: link to this property. 116 117 """ 118 # (double) escaped space at the end is to appease Sphinx 119 # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#gotchas 120 return f":class:`~bokeh.core.properties.{cls.__name__}`\\ " 121 122 @staticmethod 123 def _sphinx_model_link(name): 124 """ Generate a sphinx :class: link to given named model. 125 126 """ 127 # (double) escaped space at the end is to appease Sphinx 128 # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#gotchas 129 return f":class:`~{name}`\\ " 130 131 def _sphinx_type(self): 132 """ Generate a Sphinx-style reference to this type for documentation 133 automation purposes. 134 135 """ 136 return self._sphinx_prop_link() 137 138 def make_descriptors(self, base_name): 139 """ Return a list of ``BasicPropertyDescriptor`` instances to install 140 on a class, in order to delegate attribute access to this property. 141 142 Args: 143 name (str) : the name of the property these descriptors are for 144 145 Returns: 146 list[BasicPropertyDescriptor] 147 148 The descriptors returned are collected by the ``MetaHasProps`` 149 metaclass and added to ``HasProps`` subclasses during class creation. 150 """ 151 return [ BasicPropertyDescriptor(base_name, self) ] 152 153 def _may_have_unstable_default(self): 154 """ False if we have a default that is immutable, and will be the 155 same every time (some defaults are generated on demand by a function 156 to be called). 157 158 """ 159 return isinstance(self._default, types.FunctionType) 160 161 @classmethod 162 def _copy_default(cls, default): 163 """ Return a copy of the default, or a new value if the default 164 is specified by a function. 165 166 """ 167 if not isinstance(default, types.FunctionType): 168 return copy(default) 169 else: 170 return default() 171 172 def _raw_default(self): 173 """ Return the untransformed default value. 174 175 The raw_default() needs to be validated and transformed by 176 prepare_value() before use, and may also be replaced later by 177 subclass overrides or by themes. 178 179 """ 180 return self._copy_default(self._default) 181 182 def themed_default(self, cls, name, theme_overrides): 183 """ The default, transformed by prepare_value() and the theme overrides. 184 185 """ 186 overrides = theme_overrides 187 if overrides is None or name not in overrides: 188 overrides = cls._overridden_defaults() 189 190 if name in overrides: 191 default = self._copy_default(overrides[name]) 192 else: 193 default = self._raw_default() 194 return self.prepare_value(cls, name, default) 195 196 @property 197 def serialized(self): 198 """ Whether the property should be serialized when serializing an object. 199 200 This would be False for a "virtual" or "convenience" property that duplicates 201 information already available in other properties, for example. 202 """ 203 return self._serialized 204 205 @property 206 def readonly(self): 207 """ Whether this property is read-only. 208 209 Read-only properties may only be modified by the client (i.e., by BokehJS 210 in the browser). 211 212 """ 213 return self._readonly 214 215 def matches(self, new, old): 216 """ Whether two parameters match values. 217 218 If either ``new`` or ``old`` is a NumPy array or Pandas Series or Index, 219 then the result of ``np.array_equal`` will determine if the values match. 220 221 Otherwise, the result of standard Python equality will be returned. 222 223 Returns: 224 True, if new and old match, False otherwise 225 226 """ 227 if isinstance(new, np.ndarray) or isinstance(old, np.ndarray): 228 return np.array_equal(new, old) 229 230 if pd: 231 if isinstance(new, pd.Series) or isinstance(old, pd.Series): 232 return np.array_equal(new, old) 233 234 if isinstance(new, pd.Index) or isinstance(old, pd.Index): 235 return np.array_equal(new, old) 236 237 try: 238 239 # this handles the special but common case where there is a dict with array 240 # or series as values (e.g. the .data property of a ColumnDataSource) 241 if isinstance(new, dict) and isinstance(old, dict): 242 if set(new.keys()) != set(old.keys()): 243 return False 244 return all(self.matches(new[k], old[k]) for k in new) 245 246 # FYI Numpy can erroneously raise a warning about elementwise 247 # comparison here when a timedelta is compared to another scalar. 248 # https://github.com/numpy/numpy/issues/10095 249 return new == old 250 251 # if the comparison fails for some reason, just punt and return no-match 252 except ValueError: 253 return False 254 255 def from_json(self, json, models=None): 256 """ Convert from JSON-compatible values into a value for this property. 257 258 JSON-compatible values are: list, dict, number, string, bool, None 259 260 """ 261 return json 262 263 def serialize_value(self, value): 264 """ Change the value into a JSON serializable format. 265 266 """ 267 return value 268 269 def transform(self, value): 270 """ Change the value into the canonical format for this property. 271 272 Args: 273 value (obj) : the value to apply transformation to. 274 275 Returns: 276 obj: transformed value 277 278 """ 279 return value 280 281 def validate(self, value, detail=True): 282 """ Determine whether we can set this property from this value. 283 284 Validation happens before transform() 285 286 Args: 287 value (obj) : the value to validate against this property type 288 detail (bool, options) : whether to construct detailed exceptions 289 290 Generating detailed type validation error messages can be 291 expensive. When doing type checks internally that will not 292 escape exceptions to users, these messages can be skipped 293 by setting this value to False (default: True) 294 295 Returns: 296 None 297 298 Raises: 299 ValueError if the value is not valid for this property type 300 301 """ 302 pass 303 304 def is_valid(self, value): 305 """ Whether the value passes validation 306 307 Args: 308 value (obj) : the value to validate against this property type 309 310 Returns: 311 True if valid, False otherwise 312 313 """ 314 try: 315 if validation_on(): 316 self.validate(value, False) 317 except ValueError: 318 return False 319 else: 320 return True 321 322 def wrap(self, value): 323 """ Some property types need to wrap their values in special containers, etc. 324 325 """ 326 return value 327 328 def prepare_value(self, owner: Union[HasProps, Type[HasProps]], name: str, value: Any): 329 if value is Intrinsic: 330 value = self._raw_default() 331 if value is Undefined: 332 return value 333 334 error = None 335 try: 336 if validation_on(): 337 self.validate(value) 338 except ValueError as e: 339 for tp, converter in self.alternatives: 340 if tp.is_valid(value): 341 value = converter(value) 342 break 343 else: 344 error = e 345 346 if error is None: 347 value = self.transform(value) 348 else: 349 obj_repr = owner if isinstance(owner, HasProps) else owner.__name__ 350 raise ValueError(f"failed to validate {obj_repr}.{name}: {error}") 351 352 if isinstance(owner, HasProps): 353 obj = owner 354 355 for fn, msg_or_fn in self.assertions: 356 if isinstance(fn, bool): 357 result = fn 358 else: 359 result = fn(obj, value) 360 361 assert isinstance(result, bool) 362 363 if not result: 364 if isinstance(msg_or_fn, str): 365 raise ValueError(msg_or_fn) 366 else: 367 msg_or_fn(obj, name, value) 368 369 return self.wrap(value) 370 371 @property 372 def has_ref(self): 373 return False 374 375 def accepts(self, tp, converter): 376 """ Declare that other types may be converted to this property type. 377 378 Args: 379 tp (Property) : 380 A type that may be converted automatically to this property 381 type. 382 383 converter (callable) : 384 A function accepting ``value`` to perform conversion of the 385 value to this property type. 386 387 Returns: 388 self 389 390 """ 391 392 tp = ParameterizedProperty._validate_type_param(tp) 393 self.alternatives.append((tp, converter)) 394 return self 395 396 def asserts(self, fn, msg_or_fn): 397 """ Assert that prepared values satisfy given conditions. 398 399 Assertions are intended in enforce conditions beyond simple value 400 type validation. For instance, this method can be use to assert that 401 the columns of a ``ColumnDataSource`` all collectively have the same 402 length at all times. 403 404 Args: 405 fn (callable) : 406 A function accepting ``(obj, value)`` that returns True if the value 407 passes the assertion, or False otherwise. 408 409 msg_or_fn (str or callable) : 410 A message to print in case the assertion fails, or a function 411 accepting ``(obj, name, value)`` to call in in case the assertion 412 fails. 413 414 Returns: 415 self 416 417 """ 418 self.assertions.append((fn, msg_or_fn)) 419 return self 420 421class ParameterizedProperty(Property): 422 """ A base class for Properties that have type parameters, e.g. ``List(String)``. 423 424 """ 425 426 @staticmethod 427 def _validate_type_param(type_param): 428 if isinstance(type_param, type): 429 if issubclass(type_param, Property): 430 return type_param() 431 else: 432 type_param = type_param.__name__ 433 elif isinstance(type_param, Property): 434 if type_param._help is not None: 435 raise ValueError("setting 'help' on type parameters doesn't make sense") 436 437 return type_param 438 439 raise ValueError(f"expected a Property as type parameter, got {type_param}") 440 441 @property 442 def type_params(self): 443 raise NotImplementedError("abstract method") 444 445 @property 446 def has_ref(self): 447 return any(type_param.has_ref for type_param in self.type_params) 448 449class SingleParameterizedProperty(ParameterizedProperty): 450 """ A parameterized property with a single type parameter. """ 451 452 def __init__(self, type_param, *, default=Intrinsic, help=None, serialized=None, readonly=False): 453 self.type_param = self._validate_type_param(type_param) 454 default = default if default is not Intrinsic else self.type_param._raw_default() 455 super().__init__(default=default, help=help, serialized=serialized, readonly=readonly) 456 457 @property 458 def type_params(self): 459 return [self.type_param] 460 461 def __str__(self): 462 return f"{self.__class__.__name__}({self.type_param})" 463 464 def _sphinx_type(self): 465 return f"{self._sphinx_prop_link()}({self.type_param._sphinx_type()})" 466 467 def validate(self, value: Any, detail: bool = True) -> None: 468 super().validate(value, detail=detail) 469 self.type_param.validate(value, detail=detail) 470 471 def from_json(self, json, models=None): 472 return self.type_param.from_json(json, models=models) 473 474 def transform(self, value): 475 return self.type_param.transform(value) 476 477 def wrap(self, value): 478 return self.type_param.wrap(value) 479 480 def _may_have_unstable_default(self): 481 return self.type_param._may_have_unstable_default() 482 483class PrimitiveProperty(Property): 484 """ A base class for simple property types. 485 486 Subclasses should define a class attribute ``_underlying_type`` that is 487 a tuple of acceptable type values for the property. 488 489 Example: 490 491 A trivial version of a ``Float`` property might look like: 492 493 .. code-block:: python 494 495 class Float(PrimitiveProperty): 496 _underlying_type = (numbers.Real,) 497 498 """ 499 500 _underlying_type = None 501 502 def validate(self, value, detail=True): 503 super().validate(value, detail) 504 505 if isinstance(value, self._underlying_type): 506 return 507 508 if not detail: 509 raise ValueError("") 510 511 expected_type = nice_join([ cls.__name__ for cls in self._underlying_type ]) 512 msg = f"expected a value of type {expected_type}, got {value} of type {type(value).__name__}" 513 raise ValueError(msg) 514 515 def from_json(self, json, models=None): 516 if isinstance(json, self._underlying_type): 517 return json 518 expected_type = nice_join([ cls.__name__ for cls in self._underlying_type ]) 519 msg = f"{self} expected {expected_type}, got {json} of type {type(json).__name__}" 520 raise DeserializationError(msg) 521 522 def _sphinx_type(self): 523 return self._sphinx_prop_link() 524 525class ContainerProperty(ParameterizedProperty): 526 """ A base class for Container-like type properties. 527 528 """ 529 530 def _may_have_unstable_default(self): 531 # all containers are mutable, so the default can be modified 532 return True 533 534def validation_on(): 535 """ Check if property validation is currently active 536 537 Returns: 538 bool 539 540 """ 541 return Property._should_validate 542 543#----------------------------------------------------------------------------- 544# Private API 545#----------------------------------------------------------------------------- 546 547#----------------------------------------------------------------------------- 548# Code 549#----------------------------------------------------------------------------- 550