Skip to content

Commit

Permalink
Skip empty sequences when streaming with ndarrays (#14007)
Browse files Browse the repository at this point in the history
* Skip empty sequences when streaming with ndarrays

* Add NumPy version to bokeh info

* Add release notes

* Add print_info() utility function equivalent of `bokeh info`
  • Loading branch information
mattpap committed Aug 22, 2024
1 parent d6bdac9 commit 0b08e5b
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 74 deletions.
9 changes: 9 additions & 0 deletions docs/bokeh/source/docs/dev_guide/creating_issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ Software version info
bokeh info
or alternatively use:

.. code-block:: python
from bokeh.util.info import print_info
print_info()
in your scripts and/or MREs (minimal reproducible examples).

This provides you with a list of the versions of relevant software packages.
Copy and paste this information into your bug report.

Expand Down
18 changes: 10 additions & 8 deletions docs/bokeh/source/docs/dev_guide/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,16 @@ You should see output similar to:

.. code-block:: sh
Python version : 3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:20:46)
IPython version : 7.20.0
Tornado version : 6.1
Bokeh version : 3.0.0dev1+20.g6c394d579
BokehJS static path : /opt/anaconda/envs/test/lib/python3.9/site-packages/bokeh/server/static
node.js version : v16.12.0
npm version : 7.24.2
Operating system : Linux-5.11.0-40-generic-x86_64-with-glibc2.31
Python version : 3.12.3 | packaged by conda-forge | (main, Apr 15 2024, 18:38:13) [GCC 12.3.0]
IPython version : 8.19.0
Tornado version : 6.3.3
NumPy version : 2.0.0
Bokeh version : 3.5.1
BokehJS static path : /opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static
node.js version : v20.12.2
npm version : 10.8.2
jupyter_bokeh version : (not installed)
Operating system : Linux-5.15.0-86-generic-x86_64-with-glibc2.35
Run examples
~~~~~~~~~~~~
Expand Down
54 changes: 15 additions & 39 deletions src/bokeh/command/subcommands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
.. code-block:: none
Python version : 3.11.3 | packaged by conda-forge | (main, Apr 6 2023, 08:57:19) [GCC 11.3.0]
IPython version : 8.13.2
Tornado version : 6.3
Bokeh version : 3.3.0
BokehJS static path : /opt/anaconda/envs/test/lib/python3.11/site-packages/bokeh/server/static
node.js version : v18.16.1
npm version : 9.5.1
Python version : 3.12.3 | packaged by conda-forge | (main, Apr 15 2024, 18:38:13) [GCC 12.3.0]
IPython version : 8.19.0
Tornado version : 6.3.3
NumPy version : 2.0.0
Bokeh version : 3.5.1
BokehJS static path : /opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static
node.js version : v20.12.2
npm version : 10.8.2
jupyter_bokeh version : (not installed)
Operating system : Linux-5.15.0-86-generic-x86_64-with-glibc2.35
Expand All @@ -38,7 +39,7 @@
.. code-block:: none
/opt/anaconda/envs/test/lib/python3.11/site-packages/bokeh/server/static
/opt/anaconda/envs/test/lib/python3.12/site-packages/bokeh/server/static
'''

Expand All @@ -55,15 +56,11 @@
#-----------------------------------------------------------------------------

# Standard library imports
import platform
import sys
from argparse import Namespace

# Bokeh imports
from bokeh import __version__
from bokeh.settings import settings
from bokeh.util.compiler import nodejs_version, npmjs_version
from bokeh.util.dependencies import import_optional
from bokeh.util.info import print_info

# Bokeh imports
from ..subcommand import Argument, Subcommand
Expand All @@ -76,22 +73,6 @@
'Info',
)

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

def if_installed(version_or_none: str | None) -> str:
''' helper method to optionally return module version number or not installed
:param version_or_none:
:return:
'''
return version_or_none or "(not installed)"

def _version(module_name: str, attr: str) -> str | None:
module = import_optional(module_name)
return getattr(module, attr) if module else None

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -122,21 +103,16 @@ def invoke(self, args: Namespace) -> None:
if args.static:
print(settings.bokehjs_path())
else:
newline = '\n'
print(f"Python version : {sys.version.split(newline)[0]}")
print(f"IPython version : {if_installed(_version('IPython', '__version__'))}")
print(f"Tornado version : {if_installed(_version('tornado', 'version'))}")
print(f"Bokeh version : {__version__}")
print(f"BokehJS static path : {settings.bokehjs_path()}")
print(f"node.js version : {if_installed(nodejs_version())}")
print(f"npm version : {if_installed(npmjs_version())}")
print(f"jupyter_bokeh version : {if_installed(_version('jupyter_bokeh', '__version__'))}")
print(f"Operating system : {platform.platform()}")
print_info()

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
54 changes: 42 additions & 12 deletions src/bokeh/core/property/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ class SomeModel(Model):
TYPE_CHECKING,
Any,
Iterable,
MutableSequence,
Sequence,
TypeVar,
)

# External imports
import numpy as np

if TYPE_CHECKING:
import numpy.typing as npt

from ...document import Document
from ...document.events import DocumentPatchedEvent
from ...models.sources import ColumnarDataSource
Expand Down Expand Up @@ -303,7 +307,9 @@ def symmetric_difference_update(self, s: Iterable[T]) -> None:
def update(self, *s: Iterable[T]) -> None:
super().update(*s)

class PropertyValueDict(PropertyValueContainer, dict):
T_Val = TypeVar("T_Val")

class PropertyValueDict(PropertyValueContainer, dict[str, T_Val]):
""" A dict property value container that supports change notifications on
mutating operations.
Expand Down Expand Up @@ -377,7 +383,7 @@ def setdefault(self, *args):
def update(self, *args, **kwargs):
return super().update(*args, **kwargs)

class PropertyValueColumnData(PropertyValueDict):
class PropertyValueColumnData(PropertyValueDict[Sequence[Any]]):
""" A property value container for ColumnData that supports change
notifications on mutating operations.
Expand Down Expand Up @@ -435,7 +441,7 @@ def update(self, *args, **kwargs):
return result

# don't wrap with notify_owner --- notifies owners explicitly
def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str, Any],
def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str, Sequence[Any] | npt.NDArray[Any]],
rollover: int | None = None, setter: Setter | None = None) -> None:
""" Internal implementation to handle special-casing stream events
on ``ColumnDataSource`` columns.
Expand Down Expand Up @@ -468,17 +474,41 @@ def _stream(self, doc: Document, source: ColumnarDataSource, new_data: dict[str,
# is actually the already updated value. This is because the method
# self._saved_copy() makes a shallow copy.
for k in new_data:
if isinstance(self[k], np.ndarray) or isinstance(new_data[k], np.ndarray):
data = np.append(self[k], new_data[k])
if rollover is not None and len(data) > rollover:
data = data[len(data) - rollover:]
old_seq = self[k]
new_seq = new_data[k]

if isinstance(old_seq, np.ndarray) or isinstance(new_seq, np.ndarray):
# Special case for streaming with empty arrays, to allow this:
#
# data_source = ColumnDataSource(data={"DateTime": []})
# data_source.stream({"DateTime": np.array([np.datetime64("now")]))
#
# See https://github.com/bokeh/bokeh/issues/14004.
if len(old_seq) == 0:
seq = new_seq
elif len(new_seq) == 0:
seq = old_seq
else:
seq = np.append(old_seq, new_seq)

if rollover is not None and len(seq) > rollover:
seq = seq[len(seq) - rollover:]

# call dict.__setitem__ directly, bypass wrapped version on base class
dict.__setitem__(self, k, data)
dict.__setitem__(self, k, seq)
else:
L = self[k]
L.extend(new_data[k])
if rollover is not None and len(L) > rollover:
del L[:len(L) - rollover]
def apply_rollover(seq: MutableSequence[Any]) -> None:
if rollover is not None and len(seq) > rollover:
del seq[:len(seq) - rollover]

if isinstance(old_seq, MutableSequence):
seq = old_seq
seq.extend(new_seq)
apply_rollover(seq)
else:
seq = [*old_seq, *new_seq]
apply_rollover(seq)
dict.__setitem__(self, k, seq)

from ...document.events import ColumnsStreamedEvent
self._notify_owners(old, hint=ColumnsStreamedEvent(doc, source, "data", new_data, rollover, setter))
Expand Down
8 changes: 8 additions & 0 deletions src/bokeh/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
.. automodule:: bokeh.util.hex
:members:
.. _bokeh.util.info:
``bokeh.util.info``
----------------------
.. automodule:: bokeh.util.info
:members:
.. _bokeh.util.logconfig:
``bokeh.util.logconfig``
Expand Down
85 changes: 85 additions & 0 deletions src/bokeh/util/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import platform
import sys

# Bokeh imports
from bokeh import __version__
from bokeh.settings import settings
from bokeh.util.compiler import nodejs_version, npmjs_version
from bokeh.util.dependencies import import_optional

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
"print_info",
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

def print_info() -> None:
""" Print version information about Bokeh, Python, the operating system
and a selected set of dependencies.
"""
# Keep one print() per line, so that users don't have to wait a long
# time for all libraries and dependencies to get loaded.
newline = '\n'
print(f"Python version : {sys.version.split(newline)[0]}")
print(f"IPython version : {_if_installed(_version('IPython', '__version__'))}")
print(f"Tornado version : {_if_installed(_version('tornado', 'version'))}")
print(f"NumPy version : {_if_installed(_version('numpy', '__version__'))}")
print(f"Bokeh version : {__version__}")
print(f"BokehJS static path : {settings.bokehjs_path()}")
print(f"node.js version : {_if_installed(nodejs_version())}")
print(f"npm version : {_if_installed(npmjs_version())}")
print(f"jupyter_bokeh version : {_if_installed(_version('jupyter_bokeh', '__version__'))}")
print(f"Operating system : {platform.platform()}")

#-----------------------------------------------------------------------------
# Legacy API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

def _if_installed(version_or_none: str | None) -> str:
""" Return the given version or not installed if ``None``.
"""
return version_or_none or "(not installed)"

def _version(module_name: str, attr: str) -> str | None:
""" Get the version of a module if installed.
"""
module = import_optional(module_name)
return getattr(module, attr) if module else None

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
24 changes: 9 additions & 15 deletions tests/unit/bokeh/command/subcommands/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

# Standard library imports
from os.path import join
from typing import Any

# Bokeh imports
from bokeh.command.bootstrap import main
Expand Down Expand Up @@ -66,17 +65,18 @@ def test_run(capsys: Capture) -> None:
main(["bokeh", "info"])
out, err = capsys.readouterr()
lines = out.split("\n")
assert len(lines) == 10
assert len(lines) == 11
assert lines[0].startswith("Python version")
assert lines[1].startswith("IPython version")
assert lines[2].startswith("Tornado version")
assert lines[3].startswith("Bokeh version")
assert lines[4].startswith("BokehJS static")
assert lines[5].startswith("node.js version")
assert lines[6].startswith("npm version")
assert lines[7].startswith("jupyter_bokeh version")
assert lines[8].startswith("Operating system")
assert lines[9] == ""
assert lines[3].startswith("NumPy version")
assert lines[4].startswith("Bokeh version")
assert lines[5].startswith("BokehJS static")
assert lines[6].startswith("node.js version")
assert lines[7].startswith("npm version")
assert lines[8].startswith("jupyter_bokeh version")
assert lines[9].startswith("Operating system")
assert lines[10] == ""
assert err == ""

def test_run_static(capsys: Capture) -> None:
Expand All @@ -85,12 +85,6 @@ def test_run_static(capsys: Capture) -> None:
assert err == ""
assert out.endswith(join('bokeh', 'server', 'static') + '\n')

def test__version_missing(ipython: Any) -> None:
assert scinfo._version('bokeh', '__version__') is not None
assert scinfo._version('IPython', '__version__') is not None
assert scinfo._version('tornado', 'version') is not None
assert scinfo._version('junk', 'whatever') is None

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 0b08e5b

Please sign in to comment.