#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
""" Provide the Instance property.
The Instance property is used to construct object graphs of Bokeh models,
where one Bokeh model refers to another.
"""
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations
import logging # isort:skip
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Standard library imports
import types
from importlib import import_module
from typing import (
Any,
Callable,
Generic,
TypeVar,
)
# Bokeh imports
from ..has_props import HasProps
from ..serialization import Serializable
from ._sphinx import model_link, property_link, register_type_link
from .bases import Init, Property
from .singletons import Undefined
#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------
__all__ = (
'Instance',
'InstanceDefault',
'Object',
)
T = TypeVar("T", bound=object)
S = TypeVar("S", bound=Serializable)
#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
[docs]
class Object(Property[T]):
""" Accept values that are instances of any class.
.. note::
This is primarily useful for validation purpose. Non-serializable
objects will fail regardless during the serialization process.
"""
_instance_type: type[T] | Callable[[], type[T]] | str
def __init__(self, instance_type: type[T] | Callable[[], type[T]] | str, default: Init[T] = Undefined, help: str | None = None):
if not (isinstance(instance_type, type | str) or callable(instance_type)):
raise ValueError(f"expected a type, fn() -> type, or string, got {instance_type}")
if isinstance(instance_type, type):
self._assert_type(instance_type)
self._instance_type = instance_type
super().__init__(default=default, help=help)
@staticmethod
def _assert_type(instance_type: type[Any]) -> None:
pass
def __str__(self) -> str:
class_name = self.__class__.__name__
instance_type = self.instance_type.__name__
return f"{class_name}({instance_type})"
@property
def has_ref(self) -> bool:
return True
@property
def instance_type(self) -> type[T]:
instance_type: type[Serializable]
if isinstance(self._instance_type, type):
instance_type = self._instance_type
elif isinstance(self._instance_type, str):
module, name = self._instance_type.rsplit(".", 1)
instance_type = getattr(import_module(module, "bokeh"), name)
self._assert_type(instance_type)
self._instance_type = instance_type
else:
instance_type = self._instance_type()
self._assert_type(instance_type)
self._instance_type = instance_type
return instance_type
def validate(self, value: Any, detail: bool = True) -> None:
super().validate(value, detail)
if isinstance(value, self.instance_type):
return
instance_type = self.instance_type.__name__
value_type = type(value).__name__
msg = "" if not detail else f"expected an instance of type {instance_type}, got {value} of type {value_type}"
raise ValueError(msg)
def _may_have_unstable_default(self):
# because the instance value is mutable
return self._default is not Undefined
[docs]
class Instance(Object[S]):
""" Accept values that are instances of serializable types (e.g. |HasProps|). """
@staticmethod
def _assert_type(instance_type: type[Any]) -> None:
if not (isinstance(instance_type, type) and issubclass(instance_type, Serializable)):
raise ValueError(f"expected a subclass of Serializable (e.g. HasProps), got {instance_type}")
I = TypeVar("I", bound=HasProps)
[docs]
class InstanceDefault(Generic[I]):
""" Provide a deferred initializer for Instance defaults.
This is useful for Bokeh models with Instance properties that should have
unique default values for every model instance. Using an InstanceDefault
will afford better user-facing documentation than a lambda initializer.
"""
def __init__(self, model: type[I], **kwargs: Any) -> None:
self._model = model
self._kwargs = kwargs
def __call__(self) -> I:
return self._model(**self._kwargs)
def __repr__(self) -> str:
kwargs = ", ".join(f"{key}={val}" for key, val in self._kwargs.items())
return f"<Instance: {self._model.__qualified_model__}({kwargs})>"
#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
@register_type_link(Instance)
def _sphinx_type_link(obj: Instance[Any]) -> str:
# Sphinx links may be generated during docstring processing which means
# we can't necessarily evaluate pure function (e.g. lambda) Instance
# initializers, since they may contain circular references to the (not
# yet fully defined at this point) Model
if isinstance(obj._instance_type, types.FunctionType):
return f"{property_link(obj)}"
if isinstance(obj._instance_type, str):
return f"{property_link(obj)}({obj._instance_type!r})"
model = obj.instance_type
model_name = f"{model.__module__}.{model.__name__}"
return f"{property_link(obj)}({model_link(model_name)})"