From ff3453214358b3f6c68c1b03de7bb141b7edf24f Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 25 Jun 2026 21:37:47 -0700 Subject: [PATCH 1/2] Add list_templates() to discover from_template names (#3535) Expose a public helper that returns the names from_template accepts, grouped into regions / cities / countries (or filtered by kind). Point the unknown-name error and the templates reference at it. Closes #3535 --- README.md | 2 +- docs/source/reference/templates.rst | 4 +++ xrspatial/__init__.py | 1 + xrspatial/templates.py | 56 +++++++++++++++++++++++++++-- xrspatial/tests/test_templates.py | 39 +++++++++++++++++++- 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a72343691..3765d2eda 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [from_template](xrspatial/templates.py) | Empty study-area grid for a named region (CONUS, NYC, ...), a world city (London, Tokyo, ... in its UTM zone) or country code; `preserve='area'/'shape'` picks an EPSG projection by property | Custom | ✅ | 🔼 | 🔼 | 🔼 | +| [from_template](xrspatial/templates.py) | Empty study-area grid for a named region (CONUS, NYC, ...), a world city (London, Tokyo, ... in its UTM zone) or country code; `preserve='area'/'shape'` picks an EPSG projection by property; `list_templates()` lists every accepted name | Custom | ✅ | 🔼 | 🔼 | 🔼 | ----------- diff --git a/docs/source/reference/templates.rst b/docs/source/reference/templates.rst index 2923e02ec..b07341698 100644 --- a/docs/source/reference/templates.rst +++ b/docs/source/reference/templates.rst @@ -11,9 +11,13 @@ feeds straight into the rest of the library. Cities (national capitals, major regional metros, and recognizable US secondary cities) come back as a metro bounding box in their UTM zone. +Call :func:`~xrspatial.templates.list_templates` to discover every name +``from_template`` accepts (curated regions, world cities, and country codes). + From Template ============= .. autosummary:: :toctree: _autosummary xrspatial.templates.from_template + xrspatial.templates.list_templates diff --git a/xrspatial/__init__.py b/xrspatial/__init__.py index cbb79ce6a..6e3e7257c 100644 --- a/xrspatial/__init__.py +++ b/xrspatial/__init__.py @@ -104,6 +104,7 @@ from xrspatial.surface_distance import surface_direction # noqa from xrspatial.surface_distance import surface_distance # noqa from xrspatial.templates import from_template # noqa +from xrspatial.templates import list_templates # noqa from xrspatial.terrain import generate_terrain # noqa from xrspatial.terrain_metrics import landforms # noqa from xrspatial.terrain_metrics import LANDFORM_CLASSES # noqa diff --git a/xrspatial/templates.py b/xrspatial/templates.py index ef1b31f9f..97ef9d8a3 100644 --- a/xrspatial/templates.py +++ b/xrspatial/templates.py @@ -67,9 +67,9 @@ def _resolve(name): raise ValueError( f"Unknown template {name!r}. Available named regions: {regions}. " f"{len(_CITIES)} world cities are also supported (lowercase name, e.g. " - f"'london', 'tokyo'; see the templates reference). Countries must be an " - f"ISO-3166 / GADM alpha-3 code (e.g. 'USA', 'FRA', 'JPN'); " - f"{len(_COUNTRY_BBOXES)} are supported." + f"'london', 'tokyo'). Countries must be an ISO-3166 / GADM alpha-3 code " + f"(e.g. 'USA', 'FRA', 'JPN'); {len(_COUNTRY_BBOXES)} are supported. " + f"Call xrspatial.templates.list_templates() to list every accepted name." ) @@ -199,6 +199,56 @@ def _cf_crs_attrs(crs): if cf.get(k) is not None} +_TEMPLATE_KINDS = ("regions", "cities", "countries") + + +def list_templates(kind=None): + """List the template names ``from_template`` accepts. + + Every name in the result is a valid ``from_template`` argument: region and + city names are lowercase, country codes are uppercase ISO-3166 / GADM + alpha-3. + + Parameters + ---------- + kind : {'regions', 'cities', 'countries'}, optional + Return just one group as a sorted list. When omitted (the default), + return a dict mapping each group to its sorted list of names. + + Returns + ------- + dict of str to list of str, or list of str + With ``kind=None``, ``{'regions': [...], 'cities': [...], + 'countries': [...]}``. With ``kind`` set, the sorted list for that + group. + + Examples + -------- + .. sourcecode:: python + + >>> from xrspatial.templates import list_templates + >>> names = list_templates() + >>> sorted(names) + ['cities', 'countries', 'regions'] + >>> 'conus' in names['regions'] + True + >>> 'london' in list_templates('cities') + True + """ + groups = { + "regions": sorted(_REGIONS), + "cities": sorted(_CITIES), + "countries": sorted(_COUNTRY_BBOXES), + } + if kind is None: + return groups + if kind not in groups: + raise ValueError( + f"kind must be one of {_TEMPLATE_KINDS} or None, got {kind!r}." + ) + return groups[kind] + + def from_template(name, resolution=None, *, preserve=None, backend="numpy", fill=np.nan, chunks="auto"): """Create an empty DataArray for a common study area. diff --git a/xrspatial/tests/test_templates.py b/xrspatial/tests/test_templates.py index 2bbb44e6b..b47438291 100644 --- a/xrspatial/tests/test_templates.py +++ b/xrspatial/tests/test_templates.py @@ -4,7 +4,7 @@ import pytest import xarray as xr -from xrspatial import from_template, slope +from xrspatial import from_template, list_templates, slope from xrspatial._template_data import _CITIES, _CITY_DEFAULT_RESOLUTION, _COUNTRY_BBOXES, _REGIONS from xrspatial.tests.general_checks import cuda_and_cupy_available, dask_array_available @@ -134,6 +134,43 @@ def test_unknown_name_raises(bad): from_template(bad) +def test_unknown_name_points_to_list_templates(): + # the error tells the user how to discover valid names + with pytest.raises(ValueError, match="list_templates"): + from_template("does-not-exist") + + +def test_list_templates_grouped(): + names = list_templates() + assert set(names) == {"regions", "cities", "countries"} + # each group lists exactly its registry keys, sorted + assert names["regions"] == sorted(_REGIONS) + assert names["cities"] == sorted(_CITIES) + assert names["countries"] == sorted(_COUNTRY_BBOXES) + + +@pytest.mark.parametrize( + "kind,registry", + [("regions", _REGIONS), ("cities", _CITIES), ("countries", _COUNTRY_BBOXES)], +) +def test_list_templates_kind_filter(kind, registry): + assert list_templates(kind) == sorted(registry) + + +def test_list_templates_bad_kind_raises(): + with pytest.raises(ValueError, match="kind must be one of"): + list_templates("city") + + +def test_list_templates_names_resolve(): + # every advertised name is a valid from_template argument; build one from + # each group to confirm the listed names map straight to a template + names = list_templates() + for kind in ("regions", "cities", "countries"): + agg = from_template(names[kind][0]) + assert agg.dims == ("y", "x") + + def test_nonpositive_resolution_raises(): with pytest.raises(ValueError, match="positive"): from_template("conus", resolution=0) From 2b9c6a44f84f4671aee87461a1b61fd7746a7dea Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 25 Jun 2026 21:39:28 -0700 Subject: [PATCH 2/2] Address review nit: derive kind error from groups dict (#3535) --- xrspatial/templates.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xrspatial/templates.py b/xrspatial/templates.py index 97ef9d8a3..931d581b3 100644 --- a/xrspatial/templates.py +++ b/xrspatial/templates.py @@ -199,9 +199,6 @@ def _cf_crs_attrs(crs): if cf.get(k) is not None} -_TEMPLATE_KINDS = ("regions", "cities", "countries") - - def list_templates(kind=None): """List the template names ``from_template`` accepts. @@ -244,7 +241,7 @@ def list_templates(kind=None): return groups if kind not in groups: raise ValueError( - f"kind must be one of {_TEMPLATE_KINDS} or None, got {kind!r}." + f"kind must be one of {tuple(groups)} or None, got {kind!r}." ) return groups[kind]