From 5c1ee41839ddc3a201ac991419ff3bb5b60a33e4 Mon Sep 17 00:00:00 2001 From: Aaron Zuspan Date: Fri, 14 Jul 2023 18:57:46 -0700 Subject: [PATCH 1/6] Drop 3.7 support and simplify packaging #23 - Set min Python to 3.8 - Add 3.11 to CI tests - Simplify hatch scripts - Remove redundant setup.py and setup.cfg --- .github/workflows/tests.yml | 6 +++--- pyproject.toml | 22 +++------------------- setup.cfg | 15 --------------- setup.py | 3 --- 4 files changed, 6 insertions(+), 40 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e6a7cf4..5ca6ba9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,12 @@ jobs: 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 + - 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 diff --git a/pyproject.toml b/pyproject.toml index 040a3c4..0e40098 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", @@ -44,27 +44,11 @@ include = ["/eerepr"] [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 -""" 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() From 727f33a44134096af59a1c2878972f0a77f51b68 Mon Sep 17 00:00:00 2001 From: Aaron Zuspan <50475791+aazuspan@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:44:50 -0700 Subject: [PATCH 2/6] Add pre-commit hooks #25 - Replace optional deps with pre-commit hooks - Add mypy to checks - Update code to pass all new checks - Improve type coverage - Update CI to run pre-commit hooks --- .github/scripts/make_ee_token.py | 2 +- .github/workflows/tests.yml | 31 +++++++------- .pre-commit-config.yaml | 18 ++++++++ eerepr/__init__.py | 2 - eerepr/config.py | 4 +- eerepr/html.py | 34 +++++++-------- eerepr/repr.py | 59 +++++++++++++++----------- pyproject.toml | 18 ++++---- tests/test_cache.py | 3 +- tests/test_html.py | 72 ++++++++++++++++---------------- tests/test_reprs.py | 6 ++- 11 files changed, 139 insertions(+), 110 deletions(-) create mode 100644 .pre-commit-config.yaml 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 5ca6ba9..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.8', '3.9', '3.10', '3.11' ] steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} 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/.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/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 6392544..80157ce 100644 --- a/eerepr/config.py +++ b/eerepr/config.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import json class Config: - def __init__(self, max_cache_size, max_repr_mbs): + def __init__(self, max_cache_size: int | None, max_repr_mbs: int): self.max_cache_size = max_cache_size self.max_repr_mbs = max_repr_mbs diff --git a/eerepr/html.py b/eerepr/html.py index a74fb6e..dc90a77 100644 --- a/eerepr/html.py +++ b/eerepr/html.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import datetime from itertools import chain -from typing import Any +from typing import Any, Hashable -# 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 = [ @@ -16,7 +18,7 @@ ] -def convert_to_html(obj: Any, key=None) -> str: +def convert_to_html(obj: Any, key: Hashable | None = None) -> str: """Converts a Python object (not list or dict) to an HTML
  • element. Parameters @@ -24,23 +26,19 @@ def convert_to_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) @@ -52,7 +50,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) @@ -67,8 +65,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} @@ -76,12 +74,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"" "
  • " ) diff --git a/eerepr/repr.py b/eerepr/repr.py index a1d0845..b79ef9a 100644 --- a/eerepr/repr.py +++ b/eerepr/repr.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import uuid from functools import _lru_cache_wrapper, lru_cache from html import escape from importlib.resources import read_text -from typing import Callable, Type, Union +from typing import Any, Union from warnings import warn import ee @@ -11,32 +13,35 @@ from eerepr.html import convert_to_html REPR_HTML = "_repr_html_" +EEObject = Union[ee.Element, ee.ComputedObject] + # Track which html reprs have been set so we can overwrite them if needed. reprs_set = set() @lru_cache(maxsize=None) -def _load_css(): +def _load_css() -> str: return read_text("eerepr.static.css", "style.css") @lru_cache(maxsize=None) -def _load_js(): - """ - Note: JS is only needed because the CSS `:has()` selector isn't well supported yet, preventing - a pure CSS solution to the collapsible lists. +def _load_js() -> str: + """Note: JS is only needed because the CSS `:has()` selector isn't well supported + yet, preventing a pure CSS solution to the collapsible lists. """ return read_text("eerepr.static.js", "script.js") -def _attach_html_repr(cls: Type, repr: Callable) -> None: - """Add a HTML repr method to an EE class. Only overwrite the method if it was set by this function.""" +def _attach_html_repr(cls: type, repr: Any) -> None: + """Add a HTML repr method to an EE class. Only overwrite the method if it was set by + this function. + """ if not hasattr(cls, REPR_HTML) or cls.__name__ in reprs_set: reprs_set.update([cls.__name__]) setattr(cls, REPR_HTML, repr) -def _is_nondeterministic(obj): +def _is_nondeterministic(obj: EEObject) -> bool: """Check if an object returns nondeterministic results which would break caching. Currently, this only tests for the case of `ee.List.shuffle(seed=False)`. @@ -48,13 +53,16 @@ def _is_nondeterministic(obj): @lru_cache(maxsize=None) -def _repr_html_(obj: Union[ee.Element, ee.ComputedObject]) -> str: +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.") + warn( + f"Getting info failed with: '{e}'. Falling back to string repr.", + stacklevel=2, + ) return f"
    {escape(repr(obj))}
    " css = _load_css() @@ -64,7 +72,7 @@ def _repr_html_(obj: Union[ee.Element, ee.ComputedObject]) -> str: return ( "
    " f"" - f"
    " + "
    " f"
      {body}
    " "
    " f"" @@ -72,29 +80,32 @@ def _repr_html_(obj: Union[ee.Element, ee.ComputedObject]) -> str: ) -def _ee_repr(obj: Union[ee.Element, ee.ComputedObject]) -> str: +def _ee_repr(obj: EEObject) -> str: """Wrapper around _repr_html_ to prevent cache hits on nondeterministic objects.""" if _is_nondeterministic(obj): - # We don't want to cache nondeterministic objects, so we'll add add a unique attribute - # that causes ee.ComputedObject.__eq__ to return False, preventing a cache hit. - setattr(obj, "_eerepr_id", uuid.uuid4()) + # We don't want to cache nondeterministic objects, so we'll add add a unique + # attribute that causes ee.ComputedObject.__eq__ to return False, preventing a + # cache hit. + obj._eerepr_id = uuid.uuid4() rep = _repr_html_(obj) mbs = len(rep) / 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 print 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 print larger objects," + " but this may cause performance issues." + ), + stacklevel=2, ) return f"
    {escape(repr(obj))}
    " return rep -def initialize(max_cache_size=None) -> None: +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. @@ -102,12 +113,12 @@ def initialize(max_cache_size=None) -> None: 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 _repr_html_ if isinstance(_repr_html_, _lru_cache_wrapper): - _repr_html_ = _repr_html_.__wrapped__ + _repr_html_ = _repr_html_.__wrapped__ # type: ignore if max_cache_size != 0: _repr_html_ = lru_cache(maxsize=max_cache_size)(_repr_html_) diff --git a/pyproject.toml b/pyproject.toml index 0e40098..d475151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,17 +22,6 @@ dependencies = [ "earthengine-api", ] -[project.optional-dependencies] -dev = [ - "black", - "ipykernel", - "isort", - "jupyterlab", - "pytest", - "pytest-cov", - "hatch", -] - [project.urls] Homepage = "https://github.com/aazuspan/eerepr" @@ -42,6 +31,9 @@ path = "eerepr/__init__.py" [tool.hatch.build.targets.sdist] include = ["/eerepr"] +[tool.hatch.envs.default] +dependencies = ["pre-commit"] + [tool.hatch.envs.test] dependencies = [ "pytest", @@ -52,3 +44,7 @@ dependencies = [ all = "pytest . {args}" cov = "pytest . --cov=eerepr {args}" +[tool.ruff] +select = ["E", "I", "F", "B", "FA", "UP", "ISC", "PT", "Q", "RET", "SIM", "PERF"] +fix = true +show-fixes = true diff --git a/tests/test_cache.py b/tests/test_cache.py index c2563e4..618251f 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -12,7 +12,8 @@ def test_disabled_cache(): 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_ diff --git a/tests/test_html.py b/tests/test_html.py index 3bd23e5..eabb743 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,16 +1,17 @@ -import os import json +import os + import ee + from eerepr.html import convert_to_html def load_info(obj): - """ - Load client-side info for an Earth Engine object. + """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. + 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() @@ -18,15 +19,15 @@ def load_info(obj): os.mkdir("./tests/data") try: - with open("./tests/data/data.json", "r") as src: + with open("./tests/data/data.json") 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: @@ -46,15 +47,12 @@ def test_image(): def test_imagecollection(): - obj = ( - ee.ImageCollection( - [ - ee.Image.constant(0), - ee.Image.constant(1), - ] - ) - .set("test_prop", 42) - ) + obj = ee.ImageCollection( + [ + ee.Image.constant(0), + ee.Image.constant(1), + ] + ).set("test_prop", 42) info = load_info(obj) rep = convert_to_html(info) @@ -64,21 +62,21 @@ def test_imagecollection(): def test_feature(): - obj = ee.Feature( - geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"} - ) + obj = ee.Feature(geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"}) info = load_info(obj) rep = convert_to_html(info) assert "Feature (Point, 1 property)" in rep + def test_empty_feature(): obj = ee.Feature(None) info = load_info(obj) rep = convert_to_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]) @@ -97,7 +95,6 @@ def test_date(): assert "2021-03-27 14:01:07" in rep - def test_filter(): obj = ee.Filter.eq("foo", "bar") info = load_info(obj) @@ -181,6 +178,7 @@ def test_polygon(): assert "Polygon (4 vertices)" in rep + def test_multipolygon(): obj = ee.Geometry.MultiPolygon( [ @@ -201,6 +199,7 @@ def test_linearring(): 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) @@ -216,6 +215,7 @@ def test_typed_obj(): assert "Foo bar" in rep + def test_band(): band_id = "B1" data_type = {"type": "PixelType", "precision": "int", "min": 0, "max": 65535} @@ -223,23 +223,23 @@ def test_band(): crs = "EPSG:32610" band1 = ee.Dictionary( - { - "id": band_id, - "data_type": data_type, - "dimensions": dimensions, - "crs": crs, - } - ) + { + "id": band_id, + "data_type": data_type, + "dimensions": dimensions, + "crs": crs, + } + ) band1_info = load_info(band1) band1_rep = convert_to_html(band1_info) assert '"B1", unsigned int16, EPSG:32610, 1830x1830 px' in band1_rep band2 = ee.Dictionary( - { - "id": band_id, - "data_type": data_type, - } - ) + { + "id": band_id, + "data_type": data_type, + } + ) band2_info = load_info(band2) band2_rep = convert_to_html(band2_info) assert '"B1", unsigned int16' in band2_rep @@ -257,4 +257,4 @@ def test_pixel_types(): assert "signed int64" in convert_to_html(load_info(ee.PixelType.int64())) custom_type = dict(type="PixelType", min=10, max=255, precision="int") - assert "int ∈ [10, 255]" in convert_to_html(load_info(ee.Dictionary(custom_type))) \ No newline at end of file + assert "int ∈ [10, 255]" in convert_to_html(load_info(ee.Dictionary(custom_type))) diff --git a/tests/test_reprs.py b/tests/test_reprs.py index 57ca61a..41ad490 100644 --- a/tests/test_reprs.py +++ b/tests/test_reprs.py @@ -1,11 +1,13 @@ import ee import pytest -import eerepr +import eerepr # noqa: F401 def test_error(): - """Test that an object that raises on getInfo falls abck to the string repr and warns.""" + """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 From 757e24d2a72dec0a7ed3a9e460ecac57b2dede8a Mon Sep 17 00:00:00 2001 From: Aaron Zuspan Date: Fri, 14 Jul 2023 22:15:37 -0700 Subject: [PATCH 3/6] Add contributing guide --- CONTRIBUTING.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e29b8d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing to eeprepr + +## Setup + +`eeprepr` uses [Hatch](https://hatch.pypa.io/latest/) for package and environment management. + +## 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 scrirpt name, e.g.: + +```bash +hatch run test:all -k feature +``` + +### Building New Tests + +New features should have unit tests. To avoid having to use `getInfo` every time a test is run against a client-side Earth Engine object, `eerepr` uses a caching function `tests.test_html.load_info` to load data. This function takes an Earth Engine object and either 1) retrieves it from the local cache in `tests/data/data.json` if it has been used before, or 2) retrieves it from the server and adds it to the cache. Objects in the cache use their serialized form as an identifying key. + +To demonstrate, let's write a new dummy test that uses a custom `ee.Image`. + +```python +from tests.test_html import load_info + +def test_my_image(): + img = ee.Image.constant(42).set("custom_property", ["a", "b", "c"]) + info = load_info(img) + + assert info +``` + +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. + +When you add a new test, be sure to commit the updated data cache. \ No newline at end of file From 2fc5ce54b48f0f060c60b82d0619c98129067bfe Mon Sep 17 00:00:00 2001 From: Aaron Zuspan Date: Sat, 15 Jul 2023 12:43:59 -0700 Subject: [PATCH 4/6] Update contributing guide --- CONTRIBUTING.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e29b8d9..3a012eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,22 @@ # 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. +`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 @@ -36,7 +50,7 @@ To measure test coverage, run: hatch run test:cov ``` -Additional arguments can be passed to `pytest` after the scrirpt name, e.g.: +Additional arguments can be passed to `pytest` after the script name, e.g.: ```bash hatch run test:all -k feature From 48c57002d6d8a0b62b53635668ceeb1fb2fc889b Mon Sep 17 00:00:00 2001 From: Aaron Zuspan <50475791+aazuspan@users.noreply.github.com> Date: Sat, 15 Jul 2023 15:48:22 -0700 Subject: [PATCH 5/6] Remove cache from version control and create caching module #27 --- .gitignore | 4 +- CONTRIBUTING.md | 15 ++++--- tests/cache.py | 36 +++++++++++++++ tests/data/data.json | 1 - tests/test_html.py | 101 +++++++++++++++---------------------------- 5 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 tests/cache.py delete mode 100644 tests/data/data.json 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/CONTRIBUTING.md b/CONTRIBUTING.md index 3a012eb..b3aa822 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,20 +58,23 @@ hatch run test:all -k feature ### Building New Tests -New features should have unit tests. To avoid having to use `getInfo` every time a test is run against a client-side Earth Engine object, `eerepr` uses a caching function `tests.test_html.load_info` to load data. This function takes an Earth Engine object and either 1) retrieves it from the local cache in `tests/data/data.json` if it has been used before, or 2) retrieves it from the server and adds it to the cache. Objects in the cache use their serialized form as an identifying key. +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. -To demonstrate, let's write a new dummy test that uses a custom `ee.Image`. +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.test_html import load_info +from tests.cache import get_info def test_my_image(): img = ee.Image.constant(42).set("custom_property", ["a", "b", "c"]) - info = load_info(img) + # Use `get_info` instead of `img.getInfo` to utilize the cache + info = get_info(img) - assert info + 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. -When you add a new test, be sure to commit the updated data cache. \ No newline at end of file +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/tests/cache.py b/tests/cache.py new file mode 100644 index 0000000..9d70573 --- /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("./tests/data"): + os.mkdir("./tests/data") + + try: + with open("./tests/data/data.json") 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, 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_html.py b/tests/test_html.py index eabb743..ce633de 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,45 +1,12 @@ -import json -import os - import ee from eerepr.html import convert_to_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") 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 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 = convert_to_html(info) assert "Image foo (1 band)" in rep assert "bands: List (1 element)" in rep @@ -53,7 +20,7 @@ def test_imagecollection(): ee.Image.constant(1), ] ).set("test_prop", 42) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "ImageCollection (2 elements)" in rep @@ -63,7 +30,7 @@ def test_imagecollection(): def test_feature(): obj = ee.Feature(geom=ee.Geometry.Point([0, 0]), opt_properties={"foo": "bar"}) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Feature (Point, 1 property)" in rep @@ -71,7 +38,7 @@ def test_feature(): def test_empty_feature(): obj = ee.Feature(None) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Feature (0 properties)" in rep @@ -80,7 +47,7 @@ def test_empty_feature(): 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 = convert_to_html(info) assert "FeatureCollection (1 element, 2 columns)" in rep @@ -88,7 +55,7 @@ def test_featurecollection(): def test_date(): obj = ee.Date("2021-03-27T14:01:07") - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Date" in rep @@ -97,7 +64,7 @@ def test_date(): def test_filter(): obj = ee.Filter.eq("foo", "bar") - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Filter.eq" in rep @@ -105,7 +72,7 @@ def test_filter(): def test_dict(): obj = ee.Dictionary({"foo": "bar"}) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Object (1 property)" in rep @@ -113,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 = convert_to_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 = convert_to_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 = convert_to_html(info) assert "13th Warrior is an underrated movie" in rep @@ -133,7 +100,7 @@ def test_string(): def test_number(): obj = ee.Number(42) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "42" in rep @@ -141,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 = convert_to_html(info) assert "Point (1.11, 2.00)" in rep @@ -149,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 = convert_to_html(info) assert "MultiPoint (2 vertices)" in rep @@ -157,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 = convert_to_html(info) assert "LineString (3 vertices)" in rep @@ -165,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 = convert_to_html(info) assert "MultiLineString" in rep @@ -173,7 +140,7 @@ 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 = convert_to_html(info) assert "Polygon (4 vertices)" in rep @@ -186,7 +153,7 @@ def test_multipolygon(): [[4, 6], [3, 2], [1, 2], [4, 6]], ] ) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "MultiPolygon (8 vertices)" in rep @@ -194,7 +161,7 @@ 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 = convert_to_html(info) assert "LinearRing (4 vertices)" in rep @@ -202,7 +169,7 @@ def test_linearring(): 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 = convert_to_html(info) assert "DateRange [2020-01-01 21:01:10, 2022-03-01 14:32:11]" in rep @@ -210,7 +177,7 @@ def test_daterange(): def test_typed_obj(): obj = ee.Dictionary({"type": "Foo", "id": "bar"}) - info = load_info(obj) + info = get_info(obj) rep = convert_to_html(info) assert "Foo bar" in rep @@ -230,7 +197,7 @@ def test_band(): "crs": crs, } ) - band1_info = load_info(band1) + band1_info = get_info(band1) band1_rep = convert_to_html(band1_info) assert '"B1", unsigned int16, EPSG:32610, 1830x1830 px' in band1_rep @@ -240,21 +207,21 @@ def test_band(): "data_type": data_type, } ) - band2_info = load_info(band2) + band2_info = get_info(band2) band2_rep = convert_to_html(band2_info) assert '"B1", unsigned int16' in band2_rep def test_pixel_types(): - assert "float" in convert_to_html(load_info(ee.PixelType.float())) - assert "double" in convert_to_html(load_info(ee.PixelType.double())) - assert "signed int8" in convert_to_html(load_info(ee.PixelType.int8())) - assert "unsigned int8" in convert_to_html(load_info(ee.PixelType.uint8())) - assert "signed int16" in convert_to_html(load_info(ee.PixelType.int16())) - assert "unsigned int16" in convert_to_html(load_info(ee.PixelType.uint16())) - assert "signed int32" in convert_to_html(load_info(ee.PixelType.int32())) - assert "unsigned int32" in convert_to_html(load_info(ee.PixelType.uint32())) - assert "signed int64" in convert_to_html(load_info(ee.PixelType.int64())) + assert "float" in convert_to_html(get_info(ee.PixelType.float())) + assert "double" in convert_to_html(get_info(ee.PixelType.double())) + assert "signed int8" in convert_to_html(get_info(ee.PixelType.int8())) + assert "unsigned int8" in convert_to_html(get_info(ee.PixelType.uint8())) + assert "signed int16" in convert_to_html(get_info(ee.PixelType.int16())) + assert "unsigned int16" in convert_to_html(get_info(ee.PixelType.uint16())) + assert "signed int32" in convert_to_html(get_info(ee.PixelType.int32())) + assert "unsigned int32" in convert_to_html(get_info(ee.PixelType.uint32())) + assert "signed int64" in convert_to_html(get_info(ee.PixelType.int64())) custom_type = dict(type="PixelType", min=10, max=255, precision="int") - assert "int ∈ [10, 255]" in convert_to_html(load_info(ee.Dictionary(custom_type))) + assert "int ∈ [10, 255]" in convert_to_html(get_info(ee.Dictionary(custom_type))) From 145f0d6245a4e1d094da68d753a3940611f672bd Mon Sep 17 00:00:00 2001 From: Aaron Zuspan Date: Sat, 15 Jul 2023 20:23:01 -0700 Subject: [PATCH 6/6] Use constant paths --- tests/cache.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/cache.py b/tests/cache.py index 9d70573..3f8d9fb 100644 --- a/tests/cache.py +++ b/tests/cache.py @@ -14,22 +14,22 @@ def get_info(obj): """ serialized = obj.serialize() - if not os.path.isdir("./tests/data"): - os.mkdir("./tests/data") + if not os.path.isdir(CACHE_DIR): + os.mkdir(CACHE_DIR) try: - with open("./tests/data/data.json") as src: + with open(CACHE_PATH) 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: + 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("./tests/data/data.json", "w") as dst: + with open(CACHE_PATH, "w") as dst: existing_data[serialized] = obj.getInfo() json.dump(existing_data, dst, indent=2)