diff --git a/src/bokeh/core/property/datetime.py b/src/bokeh/core/property/datetime.py index 8198d3ca0ad..fee8995724c 100644 --- a/src/bokeh/core/property/datetime.py +++ b/src/bokeh/core/property/datetime.py @@ -14,6 +14,10 @@ from __future__ import annotations import logging # isort:skip +from bokeh.core.property.bases import Init, Property +from bokeh.core.property.singletons import Undefined +from bokeh.util.serialization import convert_date_to_datetime + log = logging.getLogger(__name__) #----------------------------------------------------------------------------- @@ -89,12 +93,23 @@ def __init__(self, default: Init[str | datetime.date | datetime.datetime] = Unde def transform(self, value: Any) -> Any: value = super().transform(value) + + # Fast path: already a datetime.datetime + if isinstance(value, datetime.datetime): + # UTC-ize if needed (handled by convert_date_to_datetime) + return convert_date_to_datetime(value) + + # String to datetime conversion if isinstance(value, str): value = datetime.datetime.fromisoformat(value) # Handled by serialization in protocol.py for now, except for Date + return convert_date_to_datetime(value) + + # datetime.date but not datetime.datetime if isinstance(value, datetime.date): - value = convert_date_to_datetime(value) + return convert_date_to_datetime(value) + return value diff --git a/src/bokeh/util/serialization.py b/src/bokeh/util/serialization.py index a64a504d761..3ccd867d641 100644 --- a/src/bokeh/util/serialization.py +++ b/src/bokeh/util/serialization.py @@ -84,7 +84,7 @@ def __getattr__(name: str) -> Any: NP_EPOCH = np.datetime64(0, 'ms') NP_MS_DELTA = np.timedelta64(1, 'ms') -DT_EPOCH = dt.datetime.fromtimestamp(0, tz=dt.timezone.utc) +DT_EPOCH = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) __doc__ = format_docstring(__doc__, binary_array_types="\n".join(f"* ``np.{x}``" for x in BINARY_ARRAY_TYPES)) @@ -135,7 +135,8 @@ def is_timedelta_type(obj: Any) -> TypeGuard[dt.timedelta | np.timedelta64]: return isinstance(obj, (dt.timedelta, np.timedelta64)) def convert_date_to_datetime(obj: dt.date) -> float: - ''' Convert a date object to a datetime + """ Convert a date object to a datetime + Args: obj (date) : the object to convert @@ -143,8 +144,17 @@ def convert_date_to_datetime(obj: dt.date) -> float: Returns: datetime - ''' - return (dt.datetime(*obj.timetuple()[:6], tzinfo=dt.timezone.utc) - DT_EPOCH).total_seconds() * 1000 + """ + # Fast path if obj is already a datetime *with* tzinfo UTC + if isinstance(obj, dt.datetime): + if obj.tzinfo is None: + obj = obj.replace(tzinfo=dt.timezone.utc) + else: + obj = obj.astimezone(dt.timezone.utc) + return (obj - DT_EPOCH).total_seconds() * 1000 + # obj is a date (not datetime) + obj_dt = dt.datetime(obj.year, obj.month, obj.day, tzinfo=dt.timezone.utc) + return (obj_dt - DT_EPOCH).total_seconds() * 1000 def convert_timedelta_type(obj: dt.timedelta | np.timedelta64) -> float: ''' Convert any recognized timedelta value to floating point absolute