Skip to content

Commit c5e2c66

Browse files
authored
Make Optional Dependencies Clearer (#10696)
### Related Closes #10695 ### What ~This add the datafusion version we specify in our examples as a lower bound to make sure datafusion gets install with rerun before we use it. Added a simple smoke test to repro the issue then verified it passed with the install specification.~ Right now we have some optional dependencies specified with our package. However we don't handle them very carefully. So importing different parts of the package could blow up. Taking inspiration from pandas I deferred imports so we should be able to import anything and not hit errors unless we ACTUALLY use the thing with the dependency. It also makes it clearer how to resolve things since it wasn't obvious to me that our notebook was already incorporated as an optional dependency. `python -c "import rerun.notebook"` just errors. Adds `rerun[datafusion]` for the datafusion dependencies and `rerun[all]` to get all of the non-testing features. If we like this approach we should be able to update the couple of different [internal places](https://github.com/rerun-io/rerun/blob/7484e03f9a98341114c30abad49895258288df76/rerun_py/rerun_sdk/rerun/recording_stream.py#L900) where we duplicate notebook checking to use this global check approach.
1 parent 0627023 commit c5e2c66

11 files changed

Lines changed: 158 additions & 49 deletions

File tree

docs/snippets/all/archetypes/image_advanced.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
rr.log("from_file", rr.EncodedImage(path=file_path))
2323

2424
# Read with Pillow and NumPy, and log the image.
25-
image = np.array(PILImage.open(file_path))
26-
rr.log("from_pillow_rgba", rr.Image(image))
25+
image_data = np.array(PILImage.open(file_path))
26+
rr.log("from_pillow_rgba", rr.Image(image_data))
2727

2828
# Drop the alpha channel from the image.
29-
image_rgb = image[..., :3]
29+
image_rgb = image_data[..., :3]
3030
rr.log("from_pillow_rgb", rr.Image(image_rgb))
3131

3232
# Read with OpenCV.
33-
image = cv2.imread(file_path)
33+
image_cv = cv2.imread(file_path)
3434
# OpenCV uses BGR ordering, we need to make this known to Rerun.
35-
rr.log("from_opencv", rr.Image(image, color_model="BGR"))
35+
rr.log("from_opencv", rr.Image(image_cv, color_model="BGR"))

pixi.lock

Lines changed: 72 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pixi.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ numpy = ">=2" # Rerun still needs numpy <2. Enforce this outside of the pypi d
685685
# This is very similar to `pixi run py-build`, and dispatches to maturin by way of PEP621.
686686
# However, pixi doesn't know how to track the rust dependencies of the python package, so
687687
# you still need to `pixi run py-build` in the correct environment if you change the rust code.
688-
rerun-sdk = { path = "rerun_py", editable = true }
688+
rerun-sdk = { path = "rerun_py", editable = true, extras = ["all"] }
689689

690690
# The same applies to the notebook.
691691
# However, in order to build rerun-notebook and thus to even activate environments with the
@@ -735,7 +735,7 @@ numpy = ">=2" # Rerun still needs numpy <2. Enforce this outside of the pypi d
735735

736736

737737
[feature.python-pypi.pypi-dependencies]
738-
rerun-sdk = "==0.24.0a6"
738+
rerun-sdk = { version = "==0.24.0a6", extras = ["all"] }
739739
rerun-notebook = "==0.24.0a6"
740740

741741
# EXAMPLES ENVIRONMENT
@@ -759,7 +759,7 @@ umap-learn = "==0.5.7"
759759
[feature.examples-common.pypi-dependencies]
760760
# External deps
761761
av = ">=14.2.0"
762-
datafusion = "==45.2.0"
762+
datafusion = "==47.0.0"
763763
jupyter = ">=1.0"
764764
polars = ">=0.12.0"
765765

rerun_py/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ text = "MIT OR Apache-2.0"
3737
[project.optional-dependencies]
3838
tests = ["pytest==7.1.2"]
3939
notebook = ["rerun-notebook==0.25.0-alpha.1+dev"]
40+
datafusion = ["datafusion==47.0.0"]
41+
all = ["notebook", "datafusion"]
4042

4143
[project.urls]
4244
documentation = "https://www.rerun.io/docs"

rerun_py/rerun_sdk/rerun/error_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,14 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
267267
return cast(T, wrapper)
268268

269269
return decorator
270+
271+
272+
class RerunOptionalDependencyError(ImportError):
273+
"""Raised when an optional dependency is not installed."""
274+
275+
def __init__(self, package: str, optional_dep: str) -> None:
276+
super().__init__(
277+
f"'{package}' could not be imported. "
278+
f"Please install it, or install rerun as rerun[{optional_dep}]/rerun[all] "
279+
"to use this functionality."
280+
)

rerun_py/rerun_sdk/rerun/notebook.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,20 @@
1919

2020

2121
from rerun import bindings
22-
from rerun_notebook import ErrorWidget as _ErrorWidget, Viewer as _Viewer
22+
from rerun.error_utils import RerunOptionalDependencyError
2323

2424
from .event import (
2525
ViewerEvent as ViewerEvent,
2626
_viewer_event_from_json_str,
2727
)
2828
from .recording_stream import RecordingStream, get_data_recording
2929

30+
HAS_NOTEBOOK = True
31+
try:
32+
from rerun_notebook import ErrorWidget as _ErrorWidget, Viewer as _Viewer
33+
except ModuleNotFoundError:
34+
HAS_NOTEBOOK = False
35+
3036
_default_width = 640
3137
_default_height = 480
3238

@@ -111,7 +117,8 @@ def __init__(
111117
Defaults to `False` if `url` is provided, and `True` otherwise.
112118
113119
"""
114-
120+
if not HAS_NOTEBOOK:
121+
raise RerunOptionalDependencyError("rerun-notebook", "notebook")
115122
self._error_widget = _ErrorWidget()
116123
self._viewer = _Viewer(
117124
width=width if width is not None else _default_width,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""DataFusion utilities."""

rerun_py/rerun_sdk/rerun/utilities/datafusion/functions/url_generation.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
import pyarrow as pa
44
import pyarrow.compute
5-
from datafusion import Expr, ScalarUDF, col, udf
65
from rerun_bindings import DatasetEntry
76

7+
from rerun.error_utils import RerunOptionalDependencyError
8+
9+
HAS_DATAFUSION = True
10+
try:
11+
from datafusion import Expr, ScalarUDF, col, udf
12+
except ModuleNotFoundError:
13+
HAS_DATAFUSION = False
14+
815

916
def partition_url(
1017
dataset: DatasetEntry,
@@ -37,6 +44,8 @@ def partition_url(
3744
to seek along. By default this will use the same string as timestamp_col.
3845
3946
"""
47+
if not HAS_DATAFUSION:
48+
raise RerunOptionalDependencyError("datafusion", "datafusion")
4049
if partition_id_col is None:
4150
partition_id_col = col("rerun_partition_id")
4251
if isinstance(partition_id_col, str):
@@ -63,6 +72,8 @@ def partition_url_udf(dataset: DatasetEntry) -> ScalarUDF:
6372
This function will generate a UDF that expects one column of input,
6473
a string containing the Partition ID.
6574
"""
75+
if not HAS_DATAFUSION:
76+
raise RerunOptionalDependencyError("datafusion", "datafusion")
6677

6778
def inner_udf(partition_id_arr: pa.Array) -> pa.Array:
6879
return pa.compute.binary_join_element_wise(
@@ -81,6 +92,8 @@ def partition_url_with_timeref_udf(dataset: DatasetEntry, timeline_name: str) ->
8192
This function will generate a UDF that expects two columns of input,
8293
a string containing the Partition ID and the timestamp in nanoseconds.
8394
"""
95+
if not HAS_DATAFUSION:
96+
raise RerunOptionalDependencyError("datafusion", "datafusion")
8497

8598
def inner_udf(partition_id_arr: pa.Array, timestamp_arr: pa.Array) -> pa.Array:
8699
timestamp_us = pa.compute.cast(timestamp_arr, pa.timestamp("us"))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import Mock, patch
4+
5+
import pytest
6+
from rerun.error_utils import RerunOptionalDependencyError
7+
8+
9+
def test_smoke() -> None:
10+
"""Just check that we can import the module."""
11+
12+
13+
def test_partition_url_import_normal() -> None:
14+
"""Check that we can import partition_url when datafusion is available."""
15+
from rerun.utilities.datafusion.functions.url_generation import partition_url
16+
17+
assert partition_url is not None
18+
19+
20+
def test_partition_url_without_datafusion() -> None:
21+
"""Check that calling partition_url raises RerunOptionalDependencyError when datafusion is unavailable."""
22+
# Mock the import to make datafusion unavailable
23+
with patch.dict("sys.modules", {"datafusion": None}):
24+
# Import the module - this should work
25+
from rerun.utilities.datafusion.functions.url_generation import partition_url
26+
27+
# Create a mock dataset for testing
28+
mock_dataset = Mock()
29+
30+
# But calling the function should raise an error
31+
with pytest.raises(RerunOptionalDependencyError, match="'datafusion' could not be imported"):
32+
partition_url(mock_dataset)

scripts/upload_image.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def build_image_stack(image: Image) -> list[tuple[int | None, Image]]:
8686
return image_stack
8787

8888

89-
def image_from_clipboard() -> Image:
89+
def image_from_clipboard() -> Image | None:
9090
"""
9191
Get image from the clipboard.
9292
@@ -114,7 +114,12 @@ def image_from_clipboard() -> Image:
114114
os.unlink(filepath)
115115
return im
116116
else:
117-
return PIL.ImageGrab.grabclipboard()
117+
# On windows might return a list, of files,
118+
# so return None signaling no image found.
119+
content = PIL.ImageGrab.grabclipboard()
120+
if isinstance(content, list):
121+
return None
122+
return content
118123

119124

120125
class Uploader:

0 commit comments

Comments
 (0)