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 a base class for objects that can have declarative, typed,
8serializable properties.
9
10.. note::
11    These classes form part of the very low-level machinery that implements
12    the Bokeh model and property system. It is unlikely that any of these
13    classes or their methods will be applicable to any standard usage or to
14    anyone who is not directly developing on Bokeh's own infrastructure.
15
16'''
17
18#-----------------------------------------------------------------------------
19# Boilerplate
20#-----------------------------------------------------------------------------
21import logging # isort:skip
22log = logging.getLogger(__name__)
23
24#-----------------------------------------------------------------------------
25# Imports
26#-----------------------------------------------------------------------------
27
28# Standard library imports
29import difflib
30from typing import Any, Dict, Optional
31from warnings import warn
32
33# Bokeh imports
34from ..util.string import nice_join
35from .property.alias import Alias
36from .property.descriptor_factory import PropertyDescriptorFactory
37from .property.descriptors import PropertyDescriptor, UnsetValueError
38from .property.override import Override
39from .property.singletons import Undefined
40from .property.wrappers import PropertyValueContainer
41
42#-----------------------------------------------------------------------------
43# Globals and constants
44#-----------------------------------------------------------------------------
45
46__all__ = (
47    'abstract',
48    'accumulate_dict_from_superclasses',
49    'accumulate_from_superclasses',
50    'HasProps',
51    'MetaHasProps',
52)
53
54#-----------------------------------------------------------------------------
55# General API
56#-----------------------------------------------------------------------------
57
58#-----------------------------------------------------------------------------
59# Dev API
60#-----------------------------------------------------------------------------
61
62def abstract(cls):
63    ''' A decorator to mark abstract base classes derived from |HasProps|.
64
65    '''
66    if not issubclass(cls, HasProps):
67        raise TypeError("%s is not a subclass of HasProps" % cls.__name__)
68
69    # running python with -OO will discard docstrings -> __doc__ is None
70    if cls.__doc__ is not None:
71        cls.__doc__ += _ABSTRACT_ADMONITION
72
73    return cls
74
75def is_DataModel(cls):
76    from ..model import DataModel
77    return issubclass(cls, HasProps) and getattr(cls, "__data_model__", False) and cls != DataModel
78
79class MetaHasProps(type):
80    ''' Specialize the construction of |HasProps| classes.
81
82    This class is a `metaclass`_ for |HasProps| that is responsible for
83    creating and adding the |PropertyDescriptor| instances that delegate
84    validation and serialization to |Property| attributes.
85
86    .. _metaclass: https://docs.python.org/3/reference/datamodel.html#metaclasses
87
88    '''
89    def __new__(meta_cls, class_name, bases, class_dict):
90        '''
91
92        '''
93        names_with_refs = set()
94        container_names = set()
95
96        # Now handle all the Override
97        overridden_defaults = {}
98        aliased_properties = {}
99        for name, prop in class_dict.items():
100            if isinstance(prop, Alias):
101                aliased_properties[name] = prop
102            elif isinstance(prop, Override):
103                if prop.default_overridden:
104                    overridden_defaults[name] = prop.default
105
106        for name, default in overridden_defaults.items():
107            del class_dict[name]
108
109        def make_property(target_name, help):
110            fget = lambda self: getattr(self, target_name)
111            fset = lambda self, value: setattr(self, target_name, value)
112            return property(fget, fset, None, help)
113
114        property_aliases = {}
115        for name, alias in aliased_properties.items():
116            property_aliases[name] = alias.name
117            class_dict[name] = make_property(alias.name, alias.help)
118
119        generators = dict()
120        for name, generator in class_dict.items():
121            if isinstance(generator, PropertyDescriptorFactory):
122                generators[name] = generator
123            elif isinstance(generator, type) and issubclass(generator, PropertyDescriptorFactory):
124                # Support the user adding a property without using parens,
125                # i.e. using just the Property subclass instead of an
126                # instance of the subclass
127                generators[name] = generator.autocreate()
128
129        dataspecs = {}
130        new_class_attrs = {}
131
132        for name, generator in generators.items():
133            prop_descriptors = generator.make_descriptors(name)
134            replaced_self = False
135            for prop_descriptor in prop_descriptors:
136                if prop_descriptor.name in generators:
137                    if generators[prop_descriptor.name] is generator:
138                        # a generator can replace itself, this is the
139                        # standard case like `foo = Int()`
140                        replaced_self = True
141                        prop_descriptor.add_prop_descriptor_to_class(class_name, new_class_attrs, names_with_refs, container_names, dataspecs)
142                    else:
143                        # if a generator tries to overwrite another
144                        # generator that's been explicitly provided,
145                        # use the prop that was manually provided
146                        # and ignore this one.
147                        pass
148                else:
149                    prop_descriptor.add_prop_descriptor_to_class(class_name, new_class_attrs, names_with_refs, container_names, dataspecs)
150            # if we won't overwrite ourselves anyway, delete the generator
151            if not replaced_self:
152                del class_dict[name]
153
154        class_dict.update(new_class_attrs)
155
156        class_dict["__properties__"] = list(new_class_attrs)
157        class_dict["__properties_with_refs__"] = names_with_refs
158        class_dict["__container_props__"] = container_names
159        class_dict["__property_aliases__"] = property_aliases
160        if len(overridden_defaults) > 0:
161            class_dict["__overridden_defaults__"] = overridden_defaults
162        if dataspecs:
163            class_dict["__dataspecs__"] = dataspecs
164
165        if "__example__" in class_dict:
166            path = class_dict["__example__"]
167
168            # running python with -OO will discard docstrings -> __doc__ is None
169            if "__doc__" in class_dict and class_dict["__doc__"] is not None:
170                class_dict["__doc__"] += _EXAMPLE_TEMPLATE % dict(path=path)
171
172        return super().__new__(meta_cls, class_name, bases, class_dict)
173
174    def __init__(cls, class_name, bases, nmspc):
175        if class_name == 'HasProps':
176            return
177        # Check for improperly overriding a Property attribute.
178        # Overriding makes no sense except through the Override
179        # class which can be used to tweak the default.
180        # Historically code also tried changing the Property's
181        # type or changing from Property to non-Property: these
182        # overrides are bad conceptually because the type of a
183        # read-write property is invariant.
184        cls_attrs = cls.__dict__.keys() # we do NOT want inherited attrs here
185        for attr in cls_attrs:
186            for base in bases:
187                if issubclass(base, HasProps) and attr in base.properties():
188                    warn(('Property "%s" in class %s was overridden by a class attribute ' + \
189                          '"%s" in class %s; it never makes sense to do this. ' + \
190                          'Either %s.%s or %s.%s should be removed, or %s.%s should not ' + \
191                          'be a Property, or use Override(), depending on the intended effect.') %
192                         (attr, base.__name__, attr, class_name,
193                          base.__name__, attr,
194                          class_name, attr,
195                          base.__name__, attr),
196                         RuntimeWarning, stacklevel=2)
197
198        if "__overridden_defaults__" in cls.__dict__:
199            our_props = cls.properties()
200            for key in cls.__dict__["__overridden_defaults__"].keys():
201                if key not in our_props:
202                    warn(('Override() of %s in class %s does not override anything.') % (key, class_name),
203                         RuntimeWarning, stacklevel=2)
204
205def accumulate_from_superclasses(cls, propname):
206    ''' Traverse the class hierarchy and accumulate the special sets of names
207    ``MetaHasProps`` stores on classes:
208
209    Args:
210        name (str) : name of the special attribute to collect.
211
212            Typically meaningful values are: ``__container_props__``,
213            ``__properties__``, ``__properties_with_refs__``
214
215    '''
216    cachename = "__cached_all" + propname
217    # we MUST use cls.__dict__ NOT hasattr(). hasattr() would also look at base
218    # classes, and the cache must be separate for each class
219    if cachename not in cls.__dict__:
220        s = set()
221        for c in cls.__mro__:
222            if issubclass(c, HasProps) and hasattr(c, propname):
223                base = getattr(c, propname)
224                s.update(base)
225        setattr(cls, cachename, s)
226    return cls.__dict__[cachename]
227
228def accumulate_dict_from_superclasses(cls, propname):
229    ''' Traverse the class hierarchy and accumulate the special dicts
230    ``MetaHasProps`` stores on classes:
231
232    Args:
233        name (str) : name of the special attribute to collect.
234
235            Typically meaningful values are: ``__dataspecs__``,
236            ``__overridden_defaults__``
237
238    '''
239    cachename = "__cached_all" + propname
240    # we MUST use cls.__dict__ NOT hasattr(). hasattr() would also look at base
241    # classes, and the cache must be separate for each class
242    if cachename not in cls.__dict__:
243        d = dict()
244        for c in cls.__mro__:
245            if issubclass(c, HasProps) and hasattr(c, propname):
246                base = getattr(c, propname)
247                for k,v in base.items():
248                    if k not in d:
249                        d[k] = v
250        setattr(cls, cachename, d)
251    return cls.__dict__[cachename]
252
253class HasProps(metaclass=MetaHasProps):
254    ''' Base class for all class types that have Bokeh properties.
255
256    '''
257    _initialized: bool = False
258
259    def __init__(self, **properties):
260        '''
261
262        '''
263        super().__init__()
264        self._property_values = dict()
265        self._unstable_default_values = dict()
266        self._unstable_themed_values = dict()
267
268        for name, value in properties.items():
269            setattr(self, name, value)
270
271        self._initialized = True
272
273    def __setattr__(self, name, value):
274        ''' Intercept attribute setting on HasProps in order to special case
275        a few situations:
276
277        * short circuit all property machinery for ``_private`` attributes
278        * suggest similar attribute names on attribute errors
279
280        Args:
281            name (str) : the name of the attribute to set on this object
282            value (obj) : the value to set
283
284        Returns:
285            None
286
287        '''
288        # self.properties() below can be expensive so avoid it
289        # if we're just setting a private underscore field
290        if name.startswith("_"):
291            super().__setattr__(name, value)
292            return
293
294        props = sorted(self.properties())
295        descriptor = getattr(self.__class__, name, None)
296
297        if name in props or (descriptor is not None and descriptor.fset is not None):
298            super().__setattr__(name, value)
299        else:
300            matches, text = difflib.get_close_matches(name.lower(), props), "similar"
301
302            if not matches:
303                matches, text = props, "possible"
304
305            raise AttributeError("unexpected attribute '%s' to %s, %s attributes are %s" %
306                (name, self.__class__.__name__, text, nice_join(matches)))
307
308    def __str__(self) -> str:
309        name = self.__class__.__name__
310        return f"{name}(...)"
311
312    __repr__ = __str__
313
314    def equals(self, other):
315        ''' Structural equality of models.
316
317        Args:
318            other (HasProps) : the other instance to compare to
319
320        Returns:
321            True, if properties are structurally equal, otherwise False
322
323        '''
324
325        # NOTE: don't try to use this to implement __eq__. Because then
326        # you will be tempted to implement __hash__, which would interfere
327        # with mutability of models. However, not implementing __hash__
328        # will make bokeh unusable in Python 3, where proper implementation
329        # of __hash__ is required when implementing __eq__.
330        if not isinstance(other, self.__class__):
331            return False
332        else:
333            return self.properties_with_values() == other.properties_with_values()
334
335    # TODO: this assumes that HasProps/Model are defined as in bokehjs, which
336    # isn't the case here. HasProps must be serializable through refs only.
337    @classmethod
338    def static_to_serializable(cls, serializer):
339        # TODO: resolving already visited objects should be serializer's duty
340        modelref = serializer.get_ref(cls)
341        if modelref is not None:
342            return modelref
343
344        bases = [ basecls for basecls in cls.__bases__ if is_DataModel(basecls) ]
345        if len(bases) == 0:
346            extends = None
347        elif len(bases) == 1:
348            extends = bases[0].static_to_serializable(serializer)
349        else:
350            raise RuntimeError("multiple bases are not supported")
351
352        name = cls.__view_model__
353        module = cls.__view_module__
354
355        # TODO: remove this
356        if module == "__main__" or module.split(".")[0] == "bokeh":
357            module = None
358
359        properties = []
360        overrides = []
361
362        # TODO: don't use unordered sets
363        for prop_name in cls.__properties__:
364            descriptor = cls.lookup(prop_name)
365            kind = None # TODO: serialize kinds
366            default = descriptor.property._default # TODO: private member
367            properties.append(dict(name=prop_name, kind=kind, default=default))
368
369        for prop_name, default in getattr(cls, "__overridden_defaults__", {}).items():
370            overrides.append(dict(name=prop_name, default=default))
371
372        modeldef = dict(name=name, module=module, extends=extends, properties=properties, overrides=overrides)
373        modelref = dict(name=name, module=module)
374
375        serializer.add_ref(cls, modelref, modeldef)
376        return modelref
377
378    def to_serializable(self, serializer):
379        pass # TODO: new serializer, hopefully in near future
380
381    def set_from_json(self, name, json, models=None, setter=None):
382        ''' Set a property value on this object from JSON.
383
384        Args:
385            name: (str) : name of the attribute to set
386
387            json: (JSON-value) : value to set to the attribute to
388
389            models (dict or None, optional) :
390                Mapping of model ids to models (default: None)
391
392                This is needed in cases where the attributes to update also
393                have values that have references.
394
395            setter(ClientSession or ServerSession or None, optional) :
396                This is used to prevent "boomerang" updates to Bokeh apps.
397
398                In the context of a Bokeh server application, incoming updates
399                to properties will be annotated with the session that is
400                doing the updating. This value is propagated through any
401                subsequent change notifications that the update triggers.
402                The session can compare the event setter to itself, and
403                suppress any updates that originate from itself.
404
405        Returns:
406            None
407
408        '''
409        if name in self.properties():
410            log.trace("Patching attribute %r of %r with %r", name, self, json)
411            descriptor = self.lookup(name)
412            descriptor.set_from_json(self, json, models, setter)
413        else:
414            log.warning("JSON had attr %r on obj %r, which is a client-only or invalid attribute that shouldn't have been sent", name, self)
415
416    def update(self, **kwargs):
417        ''' Updates the object's properties from the given keyword arguments.
418
419        Returns:
420            None
421
422        Examples:
423
424            The following are equivalent:
425
426            .. code-block:: python
427
428                from bokeh.models import Range1d
429
430                r = Range1d
431
432                # set properties individually:
433                r.start = 10
434                r.end = 20
435
436                # update properties together:
437                r.update(start=10, end=20)
438
439        '''
440        for k,v in kwargs.items():
441            setattr(self, k, v)
442
443    def update_from_json(self, json_attributes, models=None, setter=None):
444        ''' Updates the object's properties from a JSON attributes dictionary.
445
446        Args:
447            json_attributes: (JSON-dict) : attributes and values to update
448
449            models (dict or None, optional) :
450                Mapping of model ids to models (default: None)
451
452                This is needed in cases where the attributes to update also
453                have values that have references.
454
455            setter(ClientSession or ServerSession or None, optional) :
456                This is used to prevent "boomerang" updates to Bokeh apps.
457
458                In the context of a Bokeh server application, incoming updates
459                to properties will be annotated with the session that is
460                doing the updating. This value is propagated through any
461                subsequent change notifications that the update triggers.
462                The session can compare the event setter to itself, and
463                suppress any updates that originate from itself.
464
465        Returns:
466            None
467
468        '''
469        for k, v in json_attributes.items():
470            self.set_from_json(k, v, models, setter)
471
472    @classmethod
473    def lookup(cls, name: str, *, raises: bool = True) -> Optional[PropertyDescriptor]:
474        ''' Find the ``PropertyDescriptor`` for a Bokeh property on a class,
475        given the property name.
476
477        Args:
478            name (str) : name of the property to search for
479            raises (bool) : whether to raise or return None if missing
480
481        Returns:
482            PropertyDescriptor : descriptor for property named ``name``
483
484        '''
485        resolved_name = cls._property_aliases().get(name, name)
486        attr = getattr(cls, resolved_name, None)
487        if attr is not None:
488            return attr
489        elif not raises:
490            return None
491        else:
492            raise AttributeError(f"{cls.__name__}.{name} property descriptor does not exist")
493
494    @classmethod
495    def properties_with_refs(cls):
496        ''' Collect the names of all properties on this class that also have
497        references.
498
499        This method *always* traverses the class hierarchy and includes
500        properties defined on any parent classes.
501
502        Returns:
503            set[str] : names of properties that have references
504
505        '''
506        return accumulate_from_superclasses(cls, "__properties_with_refs__")
507
508    @classmethod
509    def properties_containers(cls):
510        ''' Collect the names of all container properties on this class.
511
512        This method *always* traverses the class hierarchy and includes
513        properties defined on any parent classes.
514
515        Returns:
516            set[str] : names of container properties
517
518        '''
519        return accumulate_from_superclasses(cls, "__container_props__")
520
521    @classmethod
522    def properties(cls, with_bases=True):
523        ''' Collect the names of properties on this class.
524
525        This method *optionally* traverses the class hierarchy and includes
526        properties defined on any parent classes.
527
528        Args:
529            with_bases (bool, optional) :
530                Whether to include properties defined on parent classes in
531                the results. (default: True)
532
533        Returns:
534           set[str] : property names
535
536        '''
537        if with_bases:
538            return accumulate_from_superclasses(cls, "__properties__")
539        else:
540            return set(cls.__properties__)
541
542    @classmethod
543    def dataspecs(cls):
544        ''' Collect the names of all ``DataSpec`` properties on this class.
545
546        This method *always* traverses the class hierarchy and includes
547        properties defined on any parent classes.
548
549        Returns:
550            set[str] : names of ``DataSpec`` properties
551
552        '''
553        return set(cls.dataspecs_with_props().keys())
554
555    @classmethod
556    def dataspecs_with_props(cls):
557        ''' Collect a dict mapping the names of all ``DataSpec`` properties
558        on this class to the associated properties.
559
560        This method *always* traverses the class hierarchy and includes
561        properties defined on any parent classes.
562
563        Returns:
564            dict[str, DataSpec] : mapping of names and ``DataSpec`` properties
565
566        '''
567        return accumulate_dict_from_superclasses(cls, "__dataspecs__")
568
569    def properties_with_values(self, *, include_defaults: bool = True, include_undefined: bool = False) -> Dict[str, Any]:
570        ''' Collect a dict mapping property names to their values.
571
572        This method *always* traverses the class hierarchy and includes
573        properties defined on any parent classes.
574
575        Non-serializable properties are skipped and property values are in
576        "serialized" format which may be slightly different from the values
577        you would normally read from the properties; the intent of this method
578        is to return the information needed to losslessly reconstitute the
579        object instance.
580
581        Args:
582            include_defaults (bool, optional) :
583                Whether to include properties that haven't been explicitly set
584                since the object was created. (default: True)
585
586        Returns:
587           dict : mapping from property names to their values
588
589        '''
590        return self.query_properties_with_values(lambda prop: prop.serialized,
591            include_defaults=include_defaults, include_undefined=include_undefined)
592
593    @classmethod
594    def _overridden_defaults(cls):
595        ''' Returns a dictionary of defaults that have been overridden.
596
597        .. note::
598            This is an implementation detail of ``Property``.
599
600        '''
601        return accumulate_dict_from_superclasses(cls, "__overridden_defaults__")
602
603    @classmethod
604    def _property_aliases(cls) -> Dict[str, str]:
605        ''' Returns a dictionary of aliased properties.
606
607        .. note::
608            This is an implementation detail of ``Property``.
609        '''
610        return accumulate_dict_from_superclasses(cls, "__property_aliases__")
611
612    def query_properties_with_values(self, query, *, include_defaults: bool = True, include_undefined: bool = False):
613        ''' Query the properties values of |HasProps| instances with a
614        predicate.
615
616        Args:
617            query (callable) :
618                A callable that accepts property descriptors and returns True
619                or False
620
621            include_defaults (bool, optional) :
622                Whether to include properties that have not been explicitly
623                set by a user (default: True)
624
625        Returns:
626            dict : mapping of property names and values for matching properties
627
628        '''
629        themed_keys = set()
630        result = dict()
631        if include_defaults:
632            keys = self.properties()
633        else:
634            # TODO (bev) For now, include unstable default values. Things rely on Instances
635            # always getting serialized, even defaults, and adding unstable defaults here
636            # accomplishes that. Unmodified defaults for property value containers will be
637            # weeded out below.
638            keys = set(self._property_values.keys()) | set(self._unstable_default_values.keys())
639            if self.themed_values():
640                themed_keys = set(self.themed_values().keys())
641                keys |= themed_keys
642
643        for key in keys:
644            descriptor = self.lookup(key)
645            if not query(descriptor):
646                continue
647
648            try:
649                value = descriptor.serializable_value(self)
650            except UnsetValueError:
651                if include_undefined:
652                    value = Undefined
653                else:
654                    continue
655            else:
656                if not include_defaults and key not in themed_keys:
657                    if isinstance(value, PropertyValueContainer) and key in self._unstable_default_values:
658                        continue
659
660            result[key] = value
661
662        return result
663
664    def themed_values(self):
665        ''' Get any theme-provided overrides.
666
667        Results are returned as a dict from property name to value, or
668        ``None`` if no theme overrides any values for this instance.
669
670        Returns:
671            dict or None
672
673        '''
674        return getattr(self, '__themed_values__', None)
675
676    def apply_theme(self, property_values):
677        ''' Apply a set of theme values which will be used rather than
678        defaults, but will not override application-set values.
679
680        The passed-in dictionary may be kept around as-is and shared with
681        other instances to save memory (so neither the caller nor the
682        |HasProps| instance should modify it).
683
684        Args:
685            property_values (dict) : theme values to use in place of defaults
686
687        Returns:
688            None
689
690        '''
691        old_dict = self.themed_values()
692
693        # if the same theme is set again, it should reuse the same dict
694        if old_dict is property_values:  # lgtm [py/comparison-using-is]
695            return
696
697        removed = set()
698        # we're doing a little song-and-dance to avoid storing __themed_values__ or
699        # an empty dict, if there's no theme that applies to this HasProps instance.
700        if old_dict is not None:
701            removed.update(set(old_dict.keys()))
702        added = set(property_values.keys())
703        old_values = dict()
704        for k in added.union(removed):
705            old_values[k] = getattr(self, k)
706
707        if len(property_values) > 0:
708            setattr(self, '__themed_values__', property_values)
709        elif hasattr(self, '__themed_values__'):
710            delattr(self, '__themed_values__')
711
712        # Property container values might be cached even if unmodified. Invalidate
713        # any cached values that are not modified at this point.
714        for k, v in old_values.items():
715            if k in self._unstable_themed_values:
716                del self._unstable_themed_values[k]
717
718        # Emit any change notifications that result
719        for k, v in old_values.items():
720            descriptor = self.lookup(k)
721            descriptor.trigger_if_changed(self, v)
722
723    def unapply_theme(self):
724        ''' Remove any themed values and restore defaults.
725
726        Returns:
727            None
728
729        '''
730        self.apply_theme(property_values=dict())
731
732    def _clone(self):
733        ''' Duplicate a HasProps object.
734
735        Values that are containers are shallow-copied.
736
737        '''
738        return self.__class__(**self._property_values)
739
740#-----------------------------------------------------------------------------
741# Private API
742#-----------------------------------------------------------------------------
743
744_ABSTRACT_ADMONITION = '''
745    .. note::
746        This is an abstract base class used to help organize the hierarchy of Bokeh
747        model types. **It is not useful to instantiate on its own.**
748
749'''
750
751# The "../../" is needed for bokeh-plot to construct the correct path to examples
752_EXAMPLE_TEMPLATE = '''
753
754    Example
755    -------
756
757    .. bokeh-plot:: ../../%(path)s
758        :source-position: below
759
760'''
761
762#-----------------------------------------------------------------------------
763# Code
764#-----------------------------------------------------------------------------
765