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