Skip to content

Commit

Permalink
Add on_error config option to allow raising EE exceptions (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
aazuspan authored Dec 6, 2024
1 parent 6f528cc commit de23d73
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 48 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ All notable changes to this project will be documented in this file.

If you're using `eerepr` through `geemap>=0.35.2`, this is [handled automatically](https://github.com/gee-community/geemap/pull/2183) by `geemap`.

### Added

- Add `on_error` parameter to `initialize` with option `raise` to throw Earth Engine exceptions instead of warning
- Add `max_repr_mbs` parameter to `initialize` to allow setting the maximum repr size for safety

### Changed

- Pure CSS collapsing (no more JS!)
- Better accessibility - reprs can be navigated by keyboard
- Optimized dict sorting (3-10% faster)
- Improved styling
- Allow setting all configuration options through `eerepr.initialize`

### Fixed

Expand All @@ -32,6 +35,7 @@ All notable changes to this project will be documented in this file.
### Removed

- Dropped Python 3.7 support
- Automatic `initialize` on import

## [0.0.4] - 2022-11-30

Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,10 @@ eerepr.initialize()
display(ee.FeatureCollection("LARSE/GEDI/GEDI02_A_002_INDEX").limit(3))
```

### Large Objects
## Configuration

> [!CAUTION]
> Just like in the Code Editor, printing huge collections can be slow and may hit memory limits. If a repr exceeds 100 Mb, `eerepr` will fallback to a string repr to avoid freezing the notebook. You can adjust this limit with `eerepr.initialize(max_repr_mbs=...)`.
`eerepr.initialize` takes a number of configuration options:

## Caching

`eerepr` uses caching to improve performance. Server data will only be requested once for each unique Earth Engine object, and all subsequent requests will be retrieved from the cache until the Jupyter session is restarted.
- `max_repr_mbs`: When an HTML repr exceeds this size (default 100 MBs), the string repr will be displayed instead to avoid freezing the notebook.
- `max_cache_size`: The maximum number of Earth Engine objects to cache. Using `None` (default) is recommended unless memory is very limited or the object is likely to change, e.g. getting the most recent image from a near-real-time collection. Caching can be disabled by setting to `0`.
- `on_error`: When an object can't be retrieved from Earth Engine, either `warn` (default) or `raise`.
5 changes: 5 additions & 0 deletions eerepr/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Literal


@dataclass
class Config:
max_cache_size: int | None = None
max_repr_mbs: int = 100
on_error: Literal["warn", "raise"] = "warn"

def update(self, **kwargs) -> Config:
if "on_error" in kwargs and kwargs["on_error"] not in ["warn", "raise"]:
raise ValueError("on_error must be 'warn' or 'raise'")

self.__dict__.update(**kwargs)
return self
36 changes: 23 additions & 13 deletions eerepr/repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import uuid
from functools import _lru_cache_wrapper, lru_cache
from html import escape
from typing import Any, Union
from typing import Any, Literal, Union
from warnings import warn

import ee
Expand Down Expand Up @@ -64,16 +64,7 @@ def _is_nondeterministic(obj: EEObject) -> bool:
@lru_cache(maxsize=None)
def _repr_html_(obj: EEObject) -> str:
"""Generate an HTML representation of an EE object."""
try:
info = obj.getInfo()
# Fall back to a string repr if getInfo fails
except ee.EEException as e:
warn(
f"Getting info failed with: '{e}'. Falling back to string repr.",
stacklevel=2,
)
return f"<pre>{escape(repr(obj))}</pre>"

info = obj.getInfo()
css = _load_css()
body = convert_to_html(info)

Expand All @@ -95,7 +86,18 @@ def _ee_repr(obj: EEObject) -> str:
# cache hit.
obj._eerepr_id = uuid.uuid4()

rep = _repr_html_(obj)
try:
rep = _repr_html_(obj)
except ee.EEException as e:
if options.on_error == "raise":
raise e from None

warn(
f"Getting info failed with: '{e}'. Falling back to string repr.",
stacklevel=2,
)
return f"<pre>{escape(repr(obj))}</pre>"

mbs = len(rep) / 1e6
if mbs > options.max_repr_mbs:
warn(
Expand All @@ -115,6 +117,7 @@ def _ee_repr(obj: EEObject) -> str:
def initialize(
max_cache_size: int | None = None,
max_repr_mbs: int = 100,
on_error: Literal["warn", "raise"] = "warn",
) -> None:
"""Attach HTML repr methods to EE objects and set the cache size.
Expand All @@ -129,9 +132,16 @@ def initialize(
The maximum HTML repr size to display, in MBs. Setting this too high may freeze
the client when printing very large objects. When a repr exceeds this size, the
string repr will be displayed instead along with a warning.
on_error : {'warn', 'raise'}, default 'warn'
Whether to raise an error or display a warning when an error occurs fetching
Earth Engine data.
"""
global _repr_html_
options.update(max_cache_size=max_cache_size, max_repr_mbs=max_repr_mbs)
options.update(
max_cache_size=max_cache_size,
max_repr_mbs=max_repr_mbs,
on_error=on_error,
)

if isinstance(_repr_html_, _lru_cache_wrapper):
_repr_html_ = _repr_html_.__wrapped__ # type: ignore
Expand Down
15 changes: 0 additions & 15 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from functools import _lru_cache_wrapper

import ee
import pytest

Expand All @@ -8,19 +6,6 @@
from tests.test_html import get_test_objects


@pytest.mark.parametrize("max_cache_size", [0, None, 1, 10])
def test_cache_params(max_cache_size):
"""
Test that the cache size is correctly set, or disabled when max_cache_size=0.
"""
eerepr.initialize(max_cache_size=max_cache_size)

if max_cache_size == 0:
assert not isinstance(eerepr.repr._repr_html_, _lru_cache_wrapper)
else:
assert eerepr.repr._repr_html_.cache_info().maxsize == max_cache_size


@pytest.mark.parametrize("obj", get_test_objects().items(), ids=lambda kv: kv[0])
def test_caching(obj):
"""
Expand Down
44 changes: 44 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from functools import _lru_cache_wrapper

import ee
import pytest

import eerepr


@pytest.mark.parametrize("max_cache_size", [0, None, 1, 10])
def test_cache_params(max_cache_size):
"""
Test that the cache size is correctly set, or disabled when max_cache_size=0.
"""
eerepr.initialize(max_cache_size=max_cache_size)

if max_cache_size == 0:
assert not isinstance(eerepr.repr._repr_html_, _lru_cache_wrapper)
else:
assert eerepr.repr._repr_html_.cache_info().maxsize == max_cache_size


def test_max_repr_mbs():
"""
Test that exceeding max_repr_mbs triggers a warning and falls back to string repr.
"""
eerepr.initialize(max_repr_mbs=0)

with pytest.warns(UserWarning, match="HTML repr size"):
rep = ee.Image.constant(0).set("system:id", "foo")._repr_html_()
assert "<pre>" in rep


def test_on_error():
"""Test that errors are correctly warned or raised based on on_error."""
invalid_obj = ee.Projection("not a real epsg")

eerepr.initialize(on_error="warn")
with pytest.warns(UserWarning, match="Getting info failed"):
rep = invalid_obj._repr_html_()
assert "Projection object" in rep

eerepr.initialize(on_error="raise")
with pytest.raises(ee.EEException):
invalid_obj._repr_html_()
13 changes: 0 additions & 13 deletions tests/test_reprs.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import ee
import pytest

import eerepr
from eerepr.repr import _repr_html_


def test_error():
"""Test that an object that raises on getInfo falls back to the string repr and
warns.
"""
eerepr.initialize()
with pytest.warns(UserWarning, match="Getting info failed"):
rep = ee.Projection("not a real epsg")._repr_html_()

assert "Projection object" in rep
eerepr.reset()


def test_full_repr(data_regression):
"""Regression test the full HTML repr (with CSS and JS) of a nested EE object."""
from tests.test_html import get_test_objects
Expand Down

0 comments on commit de23d73

Please sign in to comment.