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