Skip to content

Commit

Permalink
Add support for property overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed Feb 27, 2024
1 parent d09cf66 commit 510ecb0
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 73 deletions.
28 changes: 20 additions & 8 deletions src/bokeh/core/has_props.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,28 @@ def is_DataModel(cls: type[HasProps]) -> bool:
from ..model import DataModel
return issubclass(cls, HasProps) and getattr(cls, "__data_model__", False) and cls != DataModel

def _overridden_properties(class_dict: dict[str, Any]) -> dict[str, Any]:
overridden_properties: dict[str, Any] = {}
for name, prop in tuple(class_dict.items()):
if isinstance(prop, Override) and prop.property_overridden:
overridden_properties[name] = prop.property_
return overridden_properties

def _overridden_defaults(class_dict: dict[str, Any]) -> dict[str, Any]:
overridden_defaults: dict[str, Any] = {}
for name, prop in tuple(class_dict.items()):
if isinstance(prop, Override):
if isinstance(prop, Override) and prop.default_overridden:
del class_dict[name]
if prop.default_overridden:
overridden_defaults[name] = prop.default
overridden_defaults[name] = prop.default
return overridden_defaults

def _generators(class_dict: dict[str, Any]):
def _generators(class_dict: dict[str, Any], overrides: dict[str, Any]):
generators: dict[str, PropertyDescriptorFactory[Any]] = {}
for name, generator in tuple(class_dict.items()):
if isinstance(generator, PropertyDescriptorFactory):
if name in overrides:
del class_dict[name]
generators[name] = overrides[name]
elif isinstance(generator, PropertyDescriptorFactory):
del class_dict[name]
generators[name] = generator
return generators
Expand Down Expand Up @@ -184,15 +193,17 @@ class MetaHasProps(type):
'''

__properties__: dict[str, Property[Any]]
__overridden_properties__: dict[str, Property[Any]]
__overridden_defaults__: dict[str, Any]
__themed_values__: dict[str, Any]

def __new__(cls, class_name: str, bases: tuple[type, ...], class_dict: dict[str, Any]):
'''
'''
overridden_properties = _overridden_properties(class_dict)
overridden_defaults = _overridden_defaults(class_dict)
generators = _generators(class_dict)
generators = _generators(class_dict, overridden_properties)

properties = {}

Expand All @@ -206,6 +217,7 @@ def __new__(cls, class_name: str, bases: tuple[type, ...], class_dict: dict[str,
properties[name] = descriptor.property

class_dict["__properties__"] = properties
class_dict["__overridden_properties__"] = overridden_properties
class_dict["__overridden_defaults__"] = overridden_defaults

return super().__new__(cls, class_name, bases, class_dict)
Expand All @@ -220,15 +232,15 @@ def __init__(cls, class_name: str, bases: tuple[type, ...], _) -> None:
for base in (x for x in bases if issubclass(x, HasProps)):
base_properties.update(base.properties(_with_props=True))
own_properties = {k: v for k, v in cls.__dict__.items() if isinstance(v, PropertyDescriptor)}
redeclared = own_properties.keys() & base_properties.keys()
redeclared = (own_properties.keys() - cls.__overridden_properties__.keys()) & base_properties.keys()
if redeclared:
warn(f"Properties {redeclared!r} in class {cls.__name__} were previously declared on a parent "
"class. It never makes sense to do this. Redundant properties should be deleted here, or on "
"the parent class. Override() can be used to change a default value of a base class property.",
RuntimeWarning)

# Check for no-op Overrides
unused_overrides = cls.__overridden_defaults__.keys() - cls.properties(_with_props=True).keys()
unused_overrides = (cls.__overridden_properties__.keys() | cls.__overridden_defaults__.keys()) - cls.properties(_with_props=True).keys()
if unused_overrides:
warn(f"Overrides of {unused_overrides} in class {cls.__name__} does not override anything.", RuntimeWarning)

Expand Down
23 changes: 19 additions & 4 deletions src/bokeh/core/property/override.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
#-----------------------------------------------------------------------------

# Standard library imports
from typing import Generic, TypeVar
from typing import TYPE_CHECKING, Generic, TypeVar

# Bokeh imports
from ...util.dataclasses import NotRequired, Unspecified

if TYPE_CHECKING:
from .bases import Property

#-----------------------------------------------------------------------------
# Globals and constants
Expand Down Expand Up @@ -92,13 +98,22 @@ class Child(Parent):
"""

property_overridden: bool
property_: NotRequired[Property[T]]

default_overridden: bool
default: T
default: NotRequired[T]

def __init__(self, *, default: T) -> None:
self.default_overridden = True
def __init__(self, property: NotRequired[Property[T]] = Unspecified, *, default: NotRequired[T] = Unspecified) -> None:
self.property_overridden = property is not Unspecified
self.property_ = property

self.default_overridden = default is not Unspecified
self.default = default

if self.property_overridden and self.default_overridden:
raise ValueError("'property' and 'default' cannot be used simultaneously")

Check warning on line 115 in src/bokeh/core/property/override.py

View check run for this annotation

Codecov / codecov/patch

src/bokeh/core/property/override.py#L115

Added line #L115 was not covered by tests

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
Expand Down
80 changes: 36 additions & 44 deletions src/bokeh/models/widgets/sliders.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# Bokeh imports
from ...core.has_props import abstract
from ...core.properties import (
Any,
Bool,
Datetime,
Either,
Expand Down Expand Up @@ -96,6 +97,10 @@ def __init__(self, *args, **kwargs) -> None:
# Initial or selected value, throttled according to report only on mouseup.
# """)

value = Required(Any, help="""
Initial or selected value.
""")

orientation = Enum("horizontal", "vertical", help="""
Orient the slider either horizontally (default) or vertically.
""")
Expand Down Expand Up @@ -135,6 +140,12 @@ class NumericalSlider(AbstractSlider):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

step = Nullable(Positive(Float), default=1, help="""
The step between consecutive values for discrete sliders. If ``None``,
then the slider becomes a continuous slider and any value between
``start`` and ``end`` can be picked.
""")

format = Either(String, Instance(TickFormatter), help="""
""")

Expand All @@ -149,14 +160,14 @@ class CategoricalSlider(AbstractSlider):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

value = Override(Required(String, help="""
Initial or selected category.
"""))

categories = Required(Seq(String), help="""
A collection of categories to choose from.
""")

value = Required(String, help="""
Initial or selected value.
""")

value_throttled = Readonly(Required(String), help="""
Initial or throttled selected value.
""")
Expand All @@ -176,20 +187,14 @@ def __init__(self, *args, **kwargs) -> None:
The maximum allowable value.
""")

value = Required(Float, help="""
Initial or selected value.
""")
value = Override(Required(Float, help="""
Initial or selected number.
"""))

value_throttled = Readonly(Required(Float), help="""
Initial or selected value, throttled according to report only on mouseup.
""")

step = Nullable(Positive(Float), default=1, help="""
The step between consecutive values for discrete sliders. If ``None``,
then the slider becomes a continuous slider and any value between
``start`` and ``end`` can be picked.
""")

format = Override(default="0[.]00")

class RangeSlider(NumericalSlider):
Expand All @@ -199,9 +204,9 @@ class RangeSlider(NumericalSlider):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

value = Required(Tuple(Float, Float), help="""
Initial or selected range.
""")
value = Override(Required(Tuple(Float, Float), help="""
Initial or selected range of numbers.
"""))

value_throttled = Readonly(Required(Tuple(Float, Float)), help="""
Initial or selected value, only changed at the end of an interaction.
Expand All @@ -215,10 +220,6 @@ def __init__(self, *args, **kwargs) -> None:
The maximum allowable value.
""")

step = Float(default=1, help="""
The step between consecutive values.
""")

format = Override(default="0[.]00")

class MultiValuedSlider(NumericalSlider):
Expand All @@ -228,10 +229,9 @@ class MultiValuedSlider(NumericalSlider):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

# TODO value = Override(Required(Seq(Float)), ...)
value = Required(Seq(Float), help="""
Initial or selected values.
""")
value = Override(Required(Seq(Float), help="""
Initial or selected numbers.
"""))

value_throttled = Readonly(Required(Seq(Float)), help="""
Initial or selected values, only changed at the end of an interaction.
Expand All @@ -245,10 +245,6 @@ def __init__(self, *args, **kwargs) -> None:
The maximum allowable value.
""")

step = Float(default=1, help="""
The step between consecutive values.
""")

format = Override(default="0[.]00")

class DateSlider(NumericalSlider):
Expand Down Expand Up @@ -287,9 +283,9 @@ def value_as_date(self) -> date | None:

return self.value

value = Required(Datetime, help="""
value = Override(Required(Datetime, help="""
Initial or selected value.
""")
"""))

value_throttled = Readonly(Required(Datetime), help="""
Initial or selected value, only changed at the end of an interaction.
Expand All @@ -303,10 +299,6 @@ def value_as_date(self) -> date | None:
The maximum allowable value.
""")

step = Int(default=1, help="""
The step between consecutive values, in units of days.
""")

format = Override(default="%d %b %Y")

class DateRangeSlider(NumericalSlider):
Expand Down Expand Up @@ -358,9 +350,9 @@ def value_as_date(self) -> tuple[date, date] | None:
d2 = v2
return d1, d2

value = Required(Tuple(Datetime, Datetime), help="""
Initial or selected range.
""")
value = Override(Required(Tuple(Datetime, Datetime), help="""
Initial or selected range of dates.
"""))

value_throttled = Readonly(Required(Tuple(Datetime, Datetime)), help="""
Initial or selected value, only changed at the end of an interaction.
Expand All @@ -374,9 +366,9 @@ def value_as_date(self) -> tuple[date, date] | None:
The maximum allowable value.
""")

step = Int(default=1, help="""
step = Override(Int(default=1, help="""
The step between consecutive values, in units of days.
""")
"""))

format = Override(default="%d %b %Y")

Expand Down Expand Up @@ -405,9 +397,9 @@ def value_as_datetime(self) -> tuple[datetime, datetime] | None:
d2 = v2
return d1, d2

value = Required(Tuple(Datetime, Datetime), help="""
Initial or selected range.
""")
value = Override(Required(Tuple(Datetime, Datetime), help="""
Initial or selected range of dates and times.
"""))

value_throttled = Readonly(Required(Tuple(Datetime, Datetime)), help="""
Initial or selected value, only changed at the end of an interaction.
Expand All @@ -421,10 +413,10 @@ def value_as_datetime(self) -> tuple[datetime, datetime] | None:
The maximum allowable value.
""")

step = Int(default=3_600_000, help="""
step = Override(Int(default=3_600_000, help="""
The step between consecutive values, in units of milliseconds.
Default is one hour.
""")
"""))

# TODO step_unit = Enum("hour", ...)(default="hour")

Expand Down

0 comments on commit 510ecb0

Please sign in to comment.