diff --git a/CHANGELOG.md b/CHANGELOG.md index 61225aa..ea30443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ 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 @@ -23,7 +27,6 @@ All notable changes to this project will be documented in this file. - 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 @@ -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 diff --git a/README.md b/README.md index 4066d1f..222afdc 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/eerepr/config.py b/eerepr/config.py index 43aa1de..23a9e8e 100644 --- a/eerepr/config.py +++ b/eerepr/config.py @@ -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 diff --git a/eerepr/repr.py b/eerepr/repr.py index 7ae5110..71b18eb 100644 --- a/eerepr/repr.py +++ b/eerepr/repr.py @@ -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 @@ -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"
{escape(repr(obj))}" - + info = obj.getInfo() css = _load_css() body = convert_to_html(info) @@ -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"
{escape(repr(obj))}" + mbs = len(rep) / 1e6 if mbs > options.max_repr_mbs: warn( @@ -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. @@ -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 diff --git a/tests/test_cache.py b/tests/test_cache.py index 3babbbb..ffb60ea 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,5 +1,3 @@ -from functools import _lru_cache_wrapper - import ee import pytest @@ -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): """ diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..4a01c70 --- /dev/null +++ b/tests/test_config.py @@ -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 "
" 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_() diff --git a/tests/test_reprs.py b/tests/test_reprs.py index 005a9a5..cd22ff0 100644 --- a/tests/test_reprs.py +++ b/tests/test_reprs.py @@ -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