diff --git a/.github/scripts/make_ee_token.py b/.github/scripts/make_ee_token.py
index b02d8bb..6535165 100644
--- a/.github/scripts/make_ee_token.py
+++ b/.github/scripts/make_ee_token.py
@@ -6,4 +6,4 @@
os.makedirs(credential_dir, exist_ok=True)
with open(os.path.join(credential_dir, "credentials"), "w") as dst:
- dst.write(credentials)
\ No newline at end of file
+ dst.write(credentials)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e6a7cf4..f5227d9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,37 +1,40 @@
-# This workflow sets up the Earth Engine token and runs all tests.
+name: Test and lint
-name: tests
+on: [push, pull_request]
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
+permissions:
+ contents: read
jobs:
- build:
-
+ test:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [ '3.7', '3.8', '3.9', '3.10' ]
+ python-version: [ '3.8', '3.9', '3.10', '3.11' ]
steps:
- - uses: actions/checkout@v2
+ - name: Checkout
+ uses: actions/checkout@v3
+
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
+
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install pytest
- pip install -e .[test]
+ pip install hatch
+
- name: Store EE token
run: |
python ./.github/scripts/make_ee_token.py
env:
EE_TOKEN: ${{ secrets.EE_TOKEN }}
- - name: Test with pytest
+
+ - name: Test
run: |
- pytest .
\ No newline at end of file
+ hatch run test:all
+
+ - name: Pre-commit
+ uses: pre-commit/action@v3.0.0
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f794d2f..2fdc256 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,6 @@ __pycache__/
htmlcov/
dist/
*.egg-info/
-.tox/
\ No newline at end of file
+.tox/
+
+tests/data/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..1f33bb5
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,18 @@
+repos:
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.278
+ hooks:
+ - id: ruff
+
+ - repo: https://github.com/psf/black
+ rev: 22.10.0
+ hooks:
+ - id: black
+ args: [--line-length=88, --preview]
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.2.0
+ hooks:
+ - id: mypy
+ exclude: ^tests/|^setup.py
+ args: [--ignore-missing-imports]
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b3aa822
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+# Contributing to eeprepr
+
+Contributions are always welcome! Bugs and feature requests can be opened in the [Issues](https://github.com/aazuspan/eerepr/issues). Questions and comments can be posted in the [Discussions](https://github.com/aazuspan/eerepr/discussions). To contribute code, please open an issue to discuss implementation, then follow the guide below to get started!
+
+## Setup
+
+`eeprepr` uses [Hatch](https://hatch.pypa.io/latest/) for package and environment management. To set up a development environment, first fork and clone `eerepr`, then install `hatch` in your environment.
+
+```bash
+pip install hatch
+```
+
+This will install all required dependencies for development. You can enter the environment using:
+
+```bash
+hatch shell
+```
+
+and exit by typing `quit` or `CTRL + D`.
+
+## Pre-commit Hooks
+
+Pre-commit hooks automatically run linting, formatting, and type-checking whenever a change is commited. This ensures that the codebase is always in good shape.
+
+The command below registers the pre-commit hooks for the project so that they run before every commit.
+
+```bash
+hatch run pre-commit install
+```
+
+To run all the checks manually, you can use:
+
+```bash
+hatch run pre-commit run --all-files
+```
+
+## Testing
+
+### Running Tests
+
+You can run all tests with `pytest` using the command below:
+
+```bash
+hatch run test:all
+```
+
+To measure test coverage, run:
+
+```bash
+hatch run test:cov
+```
+
+Additional arguments can be passed to `pytest` after the script name, e.g.:
+
+```bash
+hatch run test:all -k feature
+```
+
+### Building New Tests
+
+New features should have unit tests. If your test needs to use `getInfo` to retrieve data from an Earth Engine object, you'll need to use the caching system described below.
+
+Using `getInfo` to retrieve data from an Earth Engine object can be slow and network-dependent. To speed up tests, `eerepr` uses a caching function `tests.cache.get_info` to load data. This function takes an Earth Engine object and either 1) retrieves its info from a local cache file if it has been used before, or 2) retrieves it from the server and adds it to the cache. The cache directory and file (`tests/data/data.json`) will be created automatically the first time tests are run.
+
+To demonstrate, let's write a new dummy test that checks the properties of a custom `ee.Image`.
+
+```python
+from tests.cache import get_info
+
+def test_my_image():
+ img = ee.Image.constant(42).set("custom_property", ["a", "b", "c"])
+ # Use `get_info` instead of `img.getInfo` to utilize the cache
+ info = get_info(img)
+
+ assert "custom_property" in info["properties"]
+```
+
+The first time the test is run, `getInfo` will be used to retrieve the image metadata and store it in `tests/data/data.json`. Subsequent runs will pull the data directly from the cache.
+
+Caches are kept locally and are not version-controlled, so there's no need to commit newly added objects.
\ No newline at end of file
diff --git a/eerepr/__init__.py b/eerepr/__init__.py
index 1dbca12..fa92573 100644
--- a/eerepr/__init__.py
+++ b/eerepr/__init__.py
@@ -1,5 +1,3 @@
-import ee
-
from eerepr.config import options
from eerepr.repr import initialize
diff --git a/eerepr/config.py b/eerepr/config.py
index 9a18ee7..255e39b 100644
--- a/eerepr/config.py
+++ b/eerepr/config.py
@@ -1,8 +1,12 @@
+from __future__ import annotations
+
import json
class Config:
- def __init__(self, max_cache_size, max_repr_mbs, communication_delay):
+ def __init__(
+ self, max_cache_size: int | None, max_repr_mbs: int, communication_delay: float
+ ):
self.max_cache_size = max_cache_size
self.max_repr_mbs = max_repr_mbs
self.communication_delay = communication_delay
@@ -16,6 +20,7 @@ def __repr__(self):
max_cache_size=None,
# Max size of repr content in MB to prevent performance issues
max_repr_mbs=100,
- # Minimum delay in seconds before updating widgets to prevent communication timing issues.
+ # Minimum delay in seconds before updating widgets to prevent communication timing
+ # issues. Delayed that are too low can break collapsing behavior.
communication_delay=0.1,
)
diff --git a/eerepr/html.py b/eerepr/html.py
index 09ffba3..5cd34ea 100644
--- a/eerepr/html.py
+++ b/eerepr/html.py
@@ -1,10 +1,13 @@
+from __future__ import annotations
+
import datetime
-from itertools import chain
-from typing import Any
from html import escape
+from itertools import chain
+from typing import Any, Hashable
+
import ee
-# Max number of characters to display for a list before truncating to "List (n elements)""
+# Max characters to display for a list before truncating to "List (n elements)"
MAX_INLINE_LENGTH = 50
# Sorting priority for Earth Engine properties
PROPERTY_PRIORITY = [
@@ -17,21 +20,28 @@
"properties",
]
+
def build_loading_html(obj: Any) -> str:
"""Build an HTML element to display when asynchronously loading an object."""
spinner = """
"""
- return f"{spinner} {obj.__class__.__name__} (Computing)"
+ return (
+ "{spinner} {obj.__class__.__name__} (Computing)"
+ )
def build_error_html(err: ee.EEException) -> str:
"""Build an HTML element to display an Earth Engine Exception"""
return f"
{escape(str(err))}
"
+
def build_fallback_html(obj: Any) -> str:
- """Build an HTML element to fall back to if something goes wrong in the main repr."""
+ """Build an HTML element to fall back to if something goes wrong in the main repr.
+ """
return f"
element from a Python object.
Parameters
@@ -39,23 +49,19 @@ def build_object_html(obj: Any, key=None) -> str:
obj : Any
The object to convert to HTML.
key : str, optional
- The key to prepend to the object value, in the case of a dictionary value or list element.
+ The key to prepend to the object value, in the case of a dictionary value or
+ list element.
"""
if isinstance(obj, list):
return list_to_html(obj, key)
- elif isinstance(obj, dict):
+ if isinstance(obj, dict):
return dict_to_html(obj, key)
key_html = f"{key}:" if key is not None else ""
- return (
- "
"
- f"{key_html}"
- f"{obj}"
- "
"
- )
+ return f"
{key_html}{obj}
"
-def list_to_html(obj: list, key=None) -> str:
+def list_to_html(obj: list, key: Hashable | None = None) -> str:
"""Convert a Python list to an HTML
element."""
contents = str(obj)
n = len(obj)
@@ -67,7 +73,7 @@ def list_to_html(obj: list, key=None) -> str:
return _make_collapsible_li(header, children)
-def dict_to_html(obj: dict, key=None) -> str:
+def dict_to_html(obj: dict, key: Hashable | None = None) -> str:
"""Convert a Python dictionary to an HTML
element."""
obj = _sort_dict(obj)
label = _build_label(obj)
@@ -82,8 +88,8 @@ def dict_to_html(obj: dict, key=None) -> str:
def _sort_dict(obj: dict) -> dict:
"""Sort the properties of an Earth Engine object.
- This follows the Code Editor standard where priority keys are sorted first and the rest are
- returned in alphabetical order.
+ This follows the Code Editor standard where priority keys are sorted first and the
+ rest are returned in alphabetical order.
"""
priority_keys = [k for k in PROPERTY_PRIORITY if k in obj]
start = {k: obj[k] for k in priority_keys}
@@ -91,12 +97,12 @@ def _sort_dict(obj: dict) -> dict:
return {**start, **end}
-def _make_collapsible_li(header, children) -> str:
- """Package a header and children into a collapsible list element"""
+def _make_collapsible_li(header: str, children: list) -> str:
+ """Package a header and children into a collapsible list element."""
return (
"
"
f""
+ ""
f"
{''.join(children)}
"
"
"
)
diff --git a/eerepr/repr.py b/eerepr/repr.py
index a5d5733..ffcdcf2 100644
--- a/eerepr/repr.py
+++ b/eerepr/repr.py
@@ -1,29 +1,40 @@
+from __future__ import annotations
+
+import threading
+import time
import uuid
from functools import _lru_cache_wrapper, lru_cache
from typing import Callable, Type, Union
-import threading
-import anywidget
-import traitlets
-import time
from warnings import warn
+import anywidget
import ee
+import traitlets
from eerepr.config import options
-from eerepr.html import build_object_html, build_loading_html, build_error_html, build_fallback_html
+from eerepr.html import (
+ build_error_html,
+ build_fallback_html,
+ build_loading_html,
+ build_object_html,
+)
from eerepr.utils import is_nondeterministic, load_css, load_js
+REPR_HTML = "_repr_html_"
+EEObject = Union[ee.Element, ee.ComputedObject]
+EEClass = Type[Union[ee.Element, ee.ComputedObject]]
-# Track which reprs have been set so we can overwrite them if needed.
+# Track which html reprs have been set so we can overwrite them if needed.
reprs_set = set()
-EEObject = Union[ee.Element, ee.ComputedObject]
-def _attach_repr(cls: Type, repr: Callable) -> None:
- """Add a custom repr method to an EE class. Only overwrite the method if it was set by this function."""
+def _attach_repr(cls: EEClass, repr: Callable) -> None:
+ """Add a custom repr method to an EE class. Only overwrite the method if it was set
+ by this function.
+ """
if not hasattr(cls, "_ipython_display_") or cls.__name__ in reprs_set:
reprs_set.update([cls.__name__])
- setattr(cls, "_ipython_display_", repr)
+ cls._ipython_display_ = repr
def _ipython_display_(obj: EEObject, **kwargs) -> str:
@@ -35,8 +46,8 @@ def _ipython_display_(obj: EEObject, **kwargs) -> str:
def _get_cached_repr(obj: EEObject) -> str:
"""Build or retrieve an HTML repr from an Earth Engine object."""
if is_nondeterministic(obj):
- # Prevent caching of non-deterministic objects (e.g. ee.List([]).shuffle(False)))
- setattr(obj, "_eerepr_id", uuid.uuid4())
+ # Prevent caching non-deterministic objects (e.g. ee.List([]).shuffle(False)))
+ obj._eerepr_id = uuid.uuid4()
try:
info = obj.getInfo()
@@ -47,21 +58,21 @@ def _get_cached_repr(obj: EEObject) -> str:
return content
-def initialize(max_cache_size=None) -> None:
- """Attach repr methods to EE objects and set the cache size.
+def initialize(max_cache_size: int | None = None) -> None:
+ """Attach HTML repr methods to EE objects and set the cache size.
Re-running this function will reset the cache.
Parameters
----------
max_cache_size : int, optional
- The maximum number of EE objects to cache. If None, the cache size is unlimited. Set to 0
- to disable caching.
+ The maximum number of EE objects to cache. If None, the cache size is unlimited.
+ Set to 0 to disable caching.
"""
global _get_cached_repr
# Unwrap from the LRU cache so we can reset it
if isinstance(_get_cached_repr, _lru_cache_wrapper):
- _get_cached_repr = _get_cached_repr.__wrapped__
+ _get_cached_repr = _get_cached_repr.__wrapped__ # type: ignore
# If caching is enabled, rewrap in a new LRU cache
if max_cache_size != 0:
@@ -74,7 +85,7 @@ def initialize(max_cache_size=None) -> None:
class EEReprWidget(anywidget.AnyWidget):
_esm = load_js()
_css = load_css()
-
+
content = traitlets.Unicode().tag(sync=True)
def __init__(self, obj: EEObject, *args, **kwargs):
@@ -98,15 +109,19 @@ def update_content(self) -> None:
self.set_content(rep)
def set_content(self, content: str) -> None:
- """Set the widget content, checking content size to avoid crashes from huge reprs."""
+ """Set the widget content, checking content size to avoid crashes from huge
+ reprs.
+ """
mbs = len(content) / 1e6
if mbs > options.max_repr_mbs:
warn(
message=(
- f"HTML repr size ({mbs:.0f}mB) exceeds maximum ({options.max_repr_mbs:.0f}mB), falling"
- " back to string repr. You can set `eerepr.options.max_repr_mbs` to display larger"
- " objects, but this may cause performance issues."
- )
+ f"HTML repr size ({mbs:.0f}mB) exceeds maximum"
+ f" ({options.max_repr_mbs:.0f}mB), falling back to string repr. You"
+ " can set `eerepr.options.max_repr_mbs` to display larger objects,"
+ " but this may cause performance issues."
+ ),
+ stacklevel=2,
)
content = build_fallback_html(self.obj)
@@ -125,8 +140,9 @@ def _ipython_display_(self, **kwargs):
# ipywidgets v7
super()._ipython_display_(**kwargs)
else:
- from IPython.display import display
import ipywidgets
+ from IPython.display import display
+
# ipywidgets v8
data = ipywidgets.DOMWidget._repr_mimebundle_(self, **kwargs)
display(data, raw=True)
diff --git a/eerepr/utils.py b/eerepr/utils.py
index c35c106..5cf145e 100644
--- a/eerepr/utils.py
+++ b/eerepr/utils.py
@@ -1,5 +1,5 @@
-from importlib.resources import read_text
from functools import lru_cache
+from importlib.resources import read_text
def is_nondeterministic(obj):
@@ -17,6 +17,7 @@ def is_nondeterministic(obj):
def load_css():
return read_text("eerepr.static.css", "style.css")
+
@lru_cache(maxsize=None)
def load_js():
- return read_text("eerepr.static.js", "widget.js")
\ No newline at end of file
+ return read_text("eerepr.static.js", "widget.js")
diff --git a/pyproject.toml b/pyproject.toml
index bd31410..a020fd0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version"]
description = "Code Editor-style reprs for Earth Engine data in a Jupyter notebook."
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.7"
+requires-python = ">=3.8"
authors = [{ name = "Aaron Zuspan" }]
keywords = [
"earthengine",
@@ -25,17 +25,6 @@ dependencies = [
"traitlets",
]
-[project.optional-dependencies]
-dev = [
- "black",
- "ipykernel",
- "isort",
- "jupyterlab",
- "pytest",
- "pytest-cov",
- "hatch",
-]
-
[project.urls]
Homepage = "https://github.com/aazuspan/eerepr"
@@ -45,29 +34,20 @@ path = "eerepr/__init__.py"
[tool.hatch.build.targets.sdist]
include = ["/eerepr"]
+[tool.hatch.envs.default]
+dependencies = ["pre-commit"]
+
[tool.hatch.envs.test]
dependencies = [
- "earthengine-api",
"pytest",
"pytest-cov",
- "tox",
]
[tool.hatch.envs.test.scripts]
-tests = "pytest ."
-cov = "pytest . --cov=eerepr --cov-report=html"
-view-cov = "open htmlcov/index.html"
-ci = "tox"
+all = "pytest . {args}"
+cov = "pytest . --cov=eerepr {args}"
-[tool.tox]
-legacy_tox_ini = """
-[tox]
-envlist = python3.7,python3.8,python3.9,python3.10
-[testenv]
-deps =
- pytest
- pytest-cov
- earthengine-api
-commands = pytest
-isolated_build = True
-"""
+[tool.ruff]
+select = ["E", "I", "F", "B", "FA", "UP", "ISC", "PT", "Q", "RET", "SIM", "PERF"]
+fix = true
+show-fixes = true
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 7423ce8..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,15 +0,0 @@
-[metadata]
-name = eerepr
-
-[options]
-packages = find:
-include_package_data = True
-
-[options.package_data]
-eerepr =
- static/css/*
- static/js/*
-
-[options.packages.find]
-exclude =
- tests
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 6068493..0000000
--- a/setup.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from setuptools import setup
-
-setup()
diff --git a/tests/cache.py b/tests/cache.py
new file mode 100644
index 0000000..3f8d9fb
--- /dev/null
+++ b/tests/cache.py
@@ -0,0 +1,36 @@
+import json
+import os
+
+CACHE_DIR = "./tests/data/"
+CACHE_PATH = CACHE_DIR + "data.json"
+
+
+def get_info(obj):
+ """Load client-side info for an Earth Engine object.
+
+ Info is retrieved (if available) from a local JSON file using the serialized
+ object as the key. If the data does not exist locally, it is loaded from Earth
+ Engine servers and stored for future use.
+ """
+ serialized = obj.serialize()
+
+ if not os.path.isdir(CACHE_DIR):
+ os.mkdir(CACHE_DIR)
+
+ try:
+ with open(CACHE_PATH) as src:
+ existing_data = json.load(src)
+
+ # File is missing or unreadable
+ except (FileNotFoundError, json.JSONDecodeError):
+ existing_data = {}
+ with open(CACHE_PATH, "w") as dst:
+ json.dump(existing_data, dst)
+
+ # File exists, but info does not
+ if serialized not in existing_data:
+ with open(CACHE_PATH, "w") as dst:
+ existing_data[serialized] = obj.getInfo()
+ json.dump(existing_data, dst, indent=2)
+
+ return existing_data[serialized]
diff --git a/tests/data/data.json b/tests/data/data.json
deleted file mode 100644
index 91e5c5e..0000000
--- a/tests/data/data.json
+++ /dev/null
@@ -1 +0,0 @@
-{"{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Element.set\", \"arguments\": {\"key\": {\"constantValue\": \"system:id\"}, \"object\": {\"functionInvocationValue\": {\"functionName\": \"Image.constant\", \"arguments\": {\"value\": {\"constantValue\": 0}}}}, \"value\": {\"constantValue\": \"foo\"}}}}}}": {"type": "Image", "bands": [{"id": "constant", "data_type": {"type": "PixelType", "precision": "int", "min": 0, "max": 0}, "crs": "EPSG:4326", "crs_transform": [1, 0, 0, 0, 1, 0]}], "id": "foo"}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Element.set\", \"arguments\": {\"key\": {\"constantValue\": \"test_prop\"}, \"object\": {\"functionInvocationValue\": {\"functionName\": \"ImageCollection.fromImages\", \"arguments\": {\"images\": {\"arrayValue\": {\"values\": [{\"functionInvocationValue\": {\"functionName\": \"Image.constant\", \"arguments\": {\"value\": {\"constantValue\": 0}}}}, {\"functionInvocationValue\": {\"functionName\": \"Image.constant\", \"arguments\": {\"value\": {\"constantValue\": 1}}}}]}}}}}, \"value\": {\"constantValue\": 42}}}}}}": {"type": "ImageCollection", "bands": [], "properties": {"test_prop": 42}, "features": [{"type": "Image", "bands": [{"id": "constant", "data_type": {"type": "PixelType", "precision": "int", "min": 0, "max": 0}, "crs": "EPSG:4326", "crs_transform": [1, 0, 0, 0, 1, 0]}], "properties": {"system:index": "0"}}, {"type": "Image", "bands": [{"id": "constant", "data_type": {"type": "PixelType", "precision": "int", "min": 1, "max": 1}, "crs": "EPSG:4326", "crs_transform": [1, 0, 0, 0, 1, 0]}], "properties": {"system:index": "1"}}]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Feature\", \"arguments\": {\"geometry\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.Point\", \"arguments\": {\"coordinates\": {\"constantValue\": [0, 0]}}}}, \"metadata\": {\"constantValue\": {\"foo\": \"bar\"}}}}}}}": {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0, 0]}, "properties": {"foo": "bar"}}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Feature\", \"arguments\": {}}}}}": {"type": "Feature", "geometry": null, "properties": {}}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Collection\", \"arguments\": {\"features\": {\"arrayValue\": {\"values\": [{\"functionInvocationValue\": {\"functionName\": \"Feature\", \"arguments\": {\"geometry\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.Point\", \"arguments\": {\"coordinates\": {\"constantValue\": [0, 0]}}}}, \"metadata\": {\"constantValue\": {\"foo\": \"bar\"}}}}}]}}}}}}}": {"type": "FeatureCollection", "columns": {"foo": "String", "system:index": "String"}, "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [0, 0]}, "id": "0", "properties": {"foo": "bar"}}]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Date\", \"arguments\": {\"value\": {\"constantValue\": \"2021-03-27T14:01:07\"}}}}}}": {"type": "Date", "value": 1616853667000}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"Filter.equals\", \"arguments\": {\"leftField\": {\"constantValue\": \"foo\"}, \"rightValue\": {\"constantValue\": \"bar\"}}}}}}": {"type": "Filter.eq", "rightValue": "bar", "leftField": "foo"}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": {\"foo\": \"bar\"}}}}": {"foo": "bar"}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": [1, 2, 3, 4]}}}": [1, 2, 3, 4], "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"List.sequence\", \"arguments\": {\"end\": {\"constantValue\": 20}, \"start\": {\"constantValue\": 0}, \"step\": {\"constantValue\": 1}}}}}}": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": \"13th Warrior is an underrated movie\"}}}": "13th Warrior is an underrated movie", "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": 42}}}": 42, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.Point\", \"arguments\": {\"coordinates\": {\"constantValue\": [1.112312, 2]}}}}}}": {"type": "Point", "coordinates": [1.112312, 2]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.MultiPoint\", \"arguments\": {\"coordinates\": {\"constantValue\": [[1, 1], [2, 2]]}}}}}}": {"type": "MultiPoint", "coordinates": [[1, 1], [2, 2]]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.LineString\", \"arguments\": {\"coordinates\": {\"constantValue\": [[1, 1], [2, 2], [3, 3]]}}}}}}": {"type": "LineString", "coordinates": [[1, 1], [2, 2], [3, 3]]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.MultiLineString\", \"arguments\": {\"coordinates\": {\"constantValue\": [[[0, 0], [1, 1]]]}}}}}}": {"type": "MultiLineString", "coordinates": [[[0, 0], [1, 1]]]}, "{\"result\": \"0\", \"values\": {\"1\": {\"constantValue\": [0, 0]}, \"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.Polygon\", \"arguments\": {\"coordinates\": {\"arrayValue\": {\"values\": [{\"arrayValue\": {\"values\": [{\"valueReference\": \"1\"}, {\"constantValue\": [1, 1]}, {\"constantValue\": [2, 2]}, {\"valueReference\": \"1\"}]}}]}}, \"evenOdd\": {\"constantValue\": true}}}}}}": {"type": "Polygon", "coordinates": [[[0, 0], [1, 1], [2, 2], [0, 0]]]}, "{\"result\": \"0\", \"values\": {\"1\": {\"constantValue\": [0, 0]}, \"2\": {\"constantValue\": [4, 6]}, \"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.MultiPolygon\", \"arguments\": {\"coordinates\": {\"arrayValue\": {\"values\": [{\"arrayValue\": {\"values\": [{\"arrayValue\": {\"values\": [{\"valueReference\": \"1\"}, {\"constantValue\": [1, 1]}, {\"constantValue\": [2, 2]}, {\"valueReference\": \"1\"}]}}, {\"arrayValue\": {\"values\": [{\"valueReference\": \"2\"}, {\"constantValue\": [3, 2]}, {\"constantValue\": [1, 2]}, {\"valueReference\": \"2\"}]}}]}}]}}, \"evenOdd\": {\"constantValue\": true}}}}}}": {"type": "MultiPolygon", "coordinates": [[[[0, 0], [1, 1], [2, 2], [0, 0]], [[4, 6], [3, 2], [1, 2], [4, 6]]]]}, "{\"result\": \"0\", \"values\": {\"1\": {\"constantValue\": [0, 0]}, \"0\": {\"functionInvocationValue\": {\"functionName\": \"GeometryConstructors.LinearRing\", \"arguments\": {\"coordinates\": {\"arrayValue\": {\"values\": [{\"valueReference\": \"1\"}, {\"constantValue\": [1, 1]}, {\"constantValue\": [2, 2]}, {\"valueReference\": \"1\"}]}}}}}}}": {"type": "LinearRing", "coordinates": [[0, 0], [1, 1], [2, 2], [0, 0]]}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"DateRange\", \"arguments\": {\"end\": {\"constantValue\": \"2022-03-01T14:32:11\"}, \"start\": {\"constantValue\": \"2020-01-01T21:01:10\"}}}}}}": {"type": "DateRange", "dates": [1577912470000, 1646145131000]}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": {\"id\": \"bar\", \"type\": \"Foo\"}}}}": {"id": "bar", "type": "Foo"}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": {\"crs\": \"EPSG:32610\", \"data_type\": {\"max\": 65535, \"min\": 0, \"precision\": \"int\", \"type\": \"PixelType\"}, \"dimensions\": [1830, 1830], \"id\": \"B1\"}}}}": {"crs": "EPSG:32610", "data_type": {"max": 65535, "min": 0, "precision": "int", "type": "PixelType"}, "dimensions": [1830, 1830], "id": "B1"}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": {\"data_type\": {\"max\": 65535, \"min\": 0, \"precision\": \"int\", \"type\": \"PixelType\"}, \"id\": \"B1\"}}}}": {"data_type": {"max": 65535, "min": 0, "precision": "int", "type": "PixelType"}, "id": "B1"}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.float\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "float"}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.double\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "double"}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.int8\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": -128, "max": 127}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.uint8\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": 0, "max": 255}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.int16\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": -32768, "max": 32767}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.uint16\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": 0, "max": 65535}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.int32\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": -2147483648, "max": 2147483647}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.uint32\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": 0, "max": 4294967295}, "{\"result\": \"0\", \"values\": {\"0\": {\"functionInvocationValue\": {\"functionName\": \"PixelType\", \"arguments\": {\"precision\": {\"functionInvocationValue\": {\"functionName\": \"PixelType.int64\", \"arguments\": {}}}}}}}}": {"type": "PixelType", "precision": "int", "min": -9.223372036854776e+18, "max": 9.223372036854776e+18}, "{\"result\": \"0\", \"values\": {\"0\": {\"constantValue\": {\"max\": 255, \"min\": 10, \"precision\": \"int\", \"type\": \"PixelType\"}}}}": {"max": 255, "min": 10, "precision": "int", "type": "PixelType"}}
\ No newline at end of file
diff --git a/tests/test_cache.py b/tests/test_cache.py
index c2563e4..aadca39 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -3,23 +3,26 @@
import ee
import eerepr
+from eerepr.repr import _get_cached_repr
def test_disabled_cache():
eerepr.initialize(max_cache_size=0)
x = ee.Number(0)
- assert not isinstance(x._repr_html_, _lru_cache_wrapper)
+ assert not isinstance(x._ipython_display_, _lru_cache_wrapper)
def test_nondeterministic_caching():
- """ee.List.shuffle(seed=False) is nondeterministic. Make sure it misses the cache."""
+ """ee.List.shuffle(seed=False) is nondeterministic. Make sure it misses the cache.
+ """
eerepr.initialize(max_cache_size=None)
- cache = eerepr.repr._repr_html_
- cache.cache_clear()
+ _get_cached_repr.cache_clear()
- assert cache.cache_info().misses == 0
+ assert _get_cached_repr.cache_info().misses == 0
x = ee.List([0, 1, 2]).shuffle(seed=False)
- x._repr_html_()
- x._repr_html_()
- assert cache.cache_info().misses == 2
+
+ _get_cached_repr(x)
+ _get_cached_repr(x)
+
+ assert _get_cached_repr.cache_info().misses == 2
diff --git a/tests/test_html.py b/tests/test_html.py
index 739aed5..64c6d70 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -1,44 +1,12 @@
-import os
-import json
import ee
-from eerepr.html import build_object_html
-
-
-def load_info(obj):
- """
- Load client-side info for an Earth Engine object.
-
- Info is retrieved (if available) from a local JSON file using the serialized object
- as the key. If the data does not exist locally, it is loaded from Earth Engine servers
- and stored for future use.
- """
- serialized = obj.serialize()
-
- if not os.path.isdir("./tests/data"):
- os.mkdir("./tests/data")
- try:
- with open("./tests/data/data.json", "r") as src:
- existing_data = json.load(src)
-
- # File is missing or unreadable
- except (FileNotFoundError, json.JSONDecodeError):
- existing_data = {}
- with open("./tests/data/data.json", "w") as dst:
- json.dump(existing_data, dst)
-
- # File exists, but info does not
- if serialized not in existing_data:
- with open("./tests/data/data.json", "w") as dst:
- existing_data[serialized] = obj.getInfo()
- json.dump(existing_data, dst)
-
- return existing_data[serialized]
+from eerepr.html import build_object_html
+from tests.cache import get_info
def test_image():
obj = ee.Image.constant(0).set("system:id", "foo")
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Image foo (1 band)" in rep
assert "bands: List (1 element)" in rep
@@ -46,16 +14,13 @@ def test_image():
def test_imagecollection():
- obj = (
- ee.ImageCollection(
- [
- ee.Image.constant(0),
- ee.Image.constant(1),
- ]
- )
- .set("test_prop", 42)
- )
- info = load_info(obj)
+ obj = ee.ImageCollection(
+ [
+ ee.Image.constant(0),
+ ee.Image.constant(1),
+ ]
+ ).set("test_prop", 42)
+ info = get_info(obj)
rep = build_object_html(info)
assert "ImageCollection (2 elements)" in rep
@@ -64,25 +29,25 @@ def test_imagecollection():
def test_feature():
- obj = ee.Feature(
- geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"}
- )
- info = load_info(obj)
+ obj = ee.Feature(geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"})
+ info = get_info(obj)
rep = build_object_html(info)
assert "Feature (Point, 1 property)" in rep
+
def test_empty_feature():
obj = ee.Feature(None)
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
-
+
assert "Feature (0 properties)" in rep
+
def test_featurecollection():
feat = ee.Feature(geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"})
obj = ee.FeatureCollection([feat])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "FeatureCollection (1 element, 2 columns)" in rep
@@ -90,17 +55,16 @@ def test_featurecollection():
def test_date():
obj = ee.Date("2021-03-27T14:01:07")
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Date" in rep
assert "2021-03-27 14:01:07" in rep
-
def test_filter():
obj = ee.Filter.eq("foo", "bar")
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Filter.eq" in rep
@@ -108,7 +72,7 @@ def test_filter():
def test_dict():
obj = ee.Dictionary({"foo": "bar"})
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Object (1 property)" in rep
@@ -116,19 +80,19 @@ def test_dict():
def test_list():
short_obj = ee.List([1, 2, 3, 4])
- short_info = load_info(short_obj)
+ short_info = get_info(short_obj)
short_rep = build_object_html(short_info)
assert "[1, 2, 3, 4]" in short_rep
long_obj = ee.List.sequence(0, 20, 1)
- long_info = load_info(long_obj)
+ long_info = get_info(long_obj)
long_rep = build_object_html(long_info)
assert "List (21 elements)" in long_rep
def test_string():
obj = ee.String("13th Warrior is an underrated movie")
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "13th Warrior is an underrated movie" in rep
@@ -136,7 +100,7 @@ def test_string():
def test_number():
obj = ee.Number(42)
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "42" in rep
@@ -144,7 +108,7 @@ def test_number():
def test_point():
obj = ee.Geometry.Point([1.112312, 2])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Point (1.11, 2.00)" in rep
@@ -152,7 +116,7 @@ def test_point():
def test_multipoint():
obj = ee.Geometry.MultiPoint([[1, 1], [2, 2]])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "MultiPoint (2 vertices)" in rep
@@ -160,7 +124,7 @@ def test_multipoint():
def test_linestring():
obj = ee.Geometry.LineString([[1, 1], [2, 2], [3, 3]])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "LineString (3 vertices)" in rep
@@ -168,7 +132,7 @@ def test_linestring():
def test_multilinestring():
obj = ee.Geometry.MultiLineString([[[0, 0], [1, 1]]])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "MultiLineString" in rep
@@ -176,11 +140,12 @@ def test_multilinestring():
def test_polygon():
obj = ee.Geometry.Polygon([[0, 0], [1, 1], [2, 2], [0, 0]])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Polygon (4 vertices)" in rep
+
def test_multipolygon():
obj = ee.Geometry.MultiPolygon(
[
@@ -188,7 +153,7 @@ def test_multipolygon():
[[4, 6], [3, 2], [1, 2], [4, 6]],
]
)
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "MultiPolygon (8 vertices)" in rep
@@ -196,14 +161,15 @@ def test_multipolygon():
def test_linearring():
obj = ee.Geometry.LinearRing([[0, 0], [1, 1], [2, 2], [0, 0]])
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "LinearRing (4 vertices)" in rep
+
def test_daterange():
obj = ee.DateRange("2020-01-01T21:01:10", "2022-03-01T14:32:11")
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "DateRange [2020-01-01 21:01:10, 2022-03-01 14:32:11]" in rep
@@ -211,11 +177,12 @@ def test_daterange():
def test_typed_obj():
obj = ee.Dictionary({"type": "Foo", "id": "bar"})
- info = load_info(obj)
+ info = get_info(obj)
rep = build_object_html(info)
assert "Foo bar" in rep
+
def test_band():
band_id = "B1"
data_type = {"type": "PixelType", "precision": "int", "min": 0, "max": 65535}
@@ -223,38 +190,38 @@ def test_band():
crs = "EPSG:32610"
band1 = ee.Dictionary(
- {
- "id": band_id,
- "data_type": data_type,
- "dimensions": dimensions,
- "crs": crs,
- }
- )
- band1_info = load_info(band1)
+ {
+ "id": band_id,
+ "data_type": data_type,
+ "dimensions": dimensions,
+ "crs": crs,
+ }
+ )
+ band1_info = get_info(band1)
band1_rep = build_object_html(band1_info)
assert '"B1", unsigned int16, EPSG:32610, 1830x1830 px' in band1_rep
band2 = ee.Dictionary(
- {
- "id": band_id,
- "data_type": data_type,
- }
- )
- band2_info = load_info(band2)
+ {
+ "id": band_id,
+ "data_type": data_type,
+ }
+ )
+ band2_info = get_info(band2)
band2_rep = build_object_html(band2_info)
assert '"B1", unsigned int16' in band2_rep
def test_pixel_types():
- assert "float" in build_object_html(load_info(ee.PixelType.float()))
- assert "double" in build_object_html(load_info(ee.PixelType.double()))
- assert "signed int8" in build_object_html(load_info(ee.PixelType.int8()))
- assert "unsigned int8" in build_object_html(load_info(ee.PixelType.uint8()))
- assert "signed int16" in build_object_html(load_info(ee.PixelType.int16()))
- assert "unsigned int16" in build_object_html(load_info(ee.PixelType.uint16()))
- assert "signed int32" in build_object_html(load_info(ee.PixelType.int32()))
- assert "unsigned int32" in build_object_html(load_info(ee.PixelType.uint32()))
- assert "signed int64" in build_object_html(load_info(ee.PixelType.int64()))
+ assert "float" in build_object_html(get_info(ee.PixelType.float()))
+ assert "double" in build_object_html(get_info(ee.PixelType.double()))
+ assert "signed int8" in build_object_html(get_info(ee.PixelType.int8()))
+ assert "unsigned int8" in build_object_html(get_info(ee.PixelType.uint8()))
+ assert "signed int16" in build_object_html(get_info(ee.PixelType.int16()))
+ assert "unsigned int16" in build_object_html(get_info(ee.PixelType.uint16()))
+ assert "signed int32" in build_object_html(get_info(ee.PixelType.int32()))
+ assert "unsigned int32" in build_object_html(get_info(ee.PixelType.uint32()))
+ assert "signed int64" in build_object_html(get_info(ee.PixelType.int64()))
custom_type = dict(type="PixelType", min=10, max=255, precision="int")
- assert "int ∈ [10, 255]" in build_object_html(load_info(ee.Dictionary(custom_type)))
\ No newline at end of file
+ assert "int ∈ [10, 255]" in build_object_html(get_info(ee.Dictionary(custom_type)))
diff --git a/tests/test_reprs.py b/tests/test_reprs.py
deleted file mode 100644
index 57ca61a..0000000
--- a/tests/test_reprs.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import ee
-import pytest
-
-import eerepr
-
-
-def test_error():
- """Test that an object that raises on getInfo falls abck to the string repr and warns."""
- with pytest.warns(UserWarning):
- rep = ee.Projection("not a real epsg")._repr_html_()
- assert "ee.Projection object" in rep