cf-xarray 0.9.1__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/workflows/ci.yaml +2 -2
  2. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/workflows/pypi.yaml +2 -2
  3. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.pre-commit-config.yaml +5 -5
  4. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/PKG-INFO +1 -1
  5. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/__init__.py +2 -0
  6. cf_xarray-0.9.2/cf_xarray/_version.py +1 -0
  7. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/accessor.py +118 -38
  8. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/coding.py +1 -0
  9. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/criteria.py +9 -0
  10. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/datasets.py +29 -0
  11. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/formatting.py +16 -3
  12. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/geometry.py +193 -12
  13. cf_xarray-0.9.2/cf_xarray/tests/conftest.py +55 -0
  14. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_accessor.py +25 -3
  15. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_geometry.py +28 -46
  16. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/units.py +1 -0
  17. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray.egg-info/PKG-INFO +1 -1
  18. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray.egg-info/SOURCES.txt +1 -0
  19. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/api.rst +11 -0
  20. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/bounds.md +3 -2
  21. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/coding.md +4 -0
  22. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/dsg.md +5 -0
  23. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/flags.md +4 -0
  24. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/geometry.md +76 -22
  25. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/grid_mappings.md +4 -3
  26. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/parametricz.md +6 -0
  27. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/selecting.md +9 -1
  28. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/sgrid_ugrid.md +5 -0
  29. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/units.md +5 -1
  30. cf_xarray-0.9.1/cf_xarray/_version.py +0 -1
  31. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.binder/environment.yml +0 -0
  32. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.deepsource.toml +0 -0
  33. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/dependabot.yml +0 -0
  34. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/workflows/parse_logs.py +0 -0
  35. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/workflows/testpypi-release.yaml +0 -0
  36. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.github/workflows/upstream-dev-ci.yaml +0 -0
  37. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.gitignore +0 -0
  38. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.readthedocs.yml +0 -0
  39. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/.tributors +0 -0
  40. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/CITATION.cff +0 -0
  41. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/LICENSE +0 -0
  42. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/README.rst +0 -0
  43. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/helpers.py +0 -0
  44. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/options.py +0 -0
  45. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/py.typed +0 -0
  46. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/scripts/make_doc.py +0 -0
  47. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/scripts/print_versions.py +0 -0
  48. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/sgrid.py +0 -0
  49. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/__init__.py +0 -0
  50. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_coding.py +0 -0
  51. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_helpers.py +0 -0
  52. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_options.py +0 -0
  53. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_scripts.py +0 -0
  54. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/tests/test_units.py +0 -0
  55. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray/utils.py +0 -0
  56. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray.egg-info/dependency_links.txt +0 -0
  57. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray.egg-info/requires.txt +0 -0
  58. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/cf_xarray.egg-info/top_level.txt +0 -0
  59. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/ci/doc.yml +0 -0
  60. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/ci/environment-no-optional-deps.yml +0 -0
  61. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/ci/environment.yml +0 -0
  62. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/ci/upstream-dev-env.yml +0 -0
  63. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/codecov.yml +0 -0
  64. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/2D_bounds_averaged.png +0 -0
  65. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/2D_bounds_error.png +0 -0
  66. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/2D_bounds_nonunique.png +0 -0
  67. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/Makefile +0 -0
  68. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/dataset-diagram-logo.tex +0 -0
  69. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/full-logo.png +0 -0
  70. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/logo.png +0 -0
  71. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/logo.svg +0 -0
  72. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/rich-repr-example.png +0 -0
  73. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/_static/style.css +0 -0
  74. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/cartopy_rotated_pole.png +0 -0
  75. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/conf.py +0 -0
  76. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/contributing.rst +0 -0
  77. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/coord_axes.md +0 -0
  78. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/custom-criteria.md +0 -0
  79. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/examples/introduction.ipynb +0 -0
  80. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/faq.md +0 -0
  81. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/howtouse.md +0 -0
  82. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/index.rst +0 -0
  83. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/make.bat +0 -0
  84. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/plotting.md +0 -0
  85. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/provenance.md +0 -0
  86. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/quickstart.md +0 -0
  87. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/roadmap.rst +0 -0
  88. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/doc/whats-new.rst +0 -0
  89. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/pyproject.toml +0 -0
  90. {cf_xarray-0.9.1 → cf_xarray-0.9.2}/setup.cfg +0 -0
@@ -50,7 +50,7 @@ jobs:
50
50
  run: |
51
51
  pytest -n auto --cov=./ --cov-report=xml
52
52
  - name: Upload code coverage to Codecov
53
- uses: codecov/codecov-action@v4.3.0
53
+ uses: codecov/codecov-action@v4.4.1
54
54
  with:
55
55
  file: ./coverage.xml
56
56
  flags: unittests
@@ -114,7 +114,7 @@ jobs:
114
114
  run: |
115
115
  python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
116
116
  - name: Upload mypy coverage to Codecov
117
- uses: codecov/codecov-action@v4.3.0
117
+ uses: codecov/codecov-action@v4.4.1
118
118
  with:
119
119
  file: mypy_report/cobertura.xml
120
120
  flags: mypy
@@ -72,7 +72,7 @@ jobs:
72
72
 
73
73
  - name: Publish package to TestPyPI
74
74
  if: github.event_name == 'push'
75
- uses: pypa/gh-action-pypi-publish@v1.8.12
75
+ uses: pypa/gh-action-pypi-publish@v1.8.14
76
76
  with:
77
77
  password: ${{ secrets.TESTPYPI_TOKEN }}
78
78
  repository_url: https://test.pypi.org/legacy/
@@ -96,6 +96,6 @@ jobs:
96
96
  name: releases
97
97
  path: dist
98
98
  - name: Publish package to PyPI
99
- uses: pypa/gh-action-pypi-publish@v1.8.12
99
+ uses: pypa/gh-action-pypi-publish@v1.8.14
100
100
  with:
101
101
  verbose: true
@@ -3,20 +3,20 @@ ci:
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/asottile/pyupgrade
6
- rev: v3.15.0
6
+ rev: v3.15.2
7
7
  hooks:
8
8
  - id: pyupgrade
9
9
  args: ["--py39-plus"]
10
10
 
11
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
12
12
  # Ruff version.
13
- rev: 'v0.1.9'
13
+ rev: 'v0.3.5'
14
14
  hooks:
15
15
  - id: ruff
16
16
  args: ["--show-fixes", "--fix"]
17
17
 
18
18
  - repo: https://github.com/psf/black-pre-commit-mirror
19
- rev: 23.12.1
19
+ rev: 24.3.0
20
20
  hooks:
21
21
  - id: black
22
22
 
@@ -36,7 +36,7 @@ repos:
36
36
  - mdformat-myst
37
37
 
38
38
  - repo: https://github.com/nbQA-dev/nbQA
39
- rev: 1.7.1
39
+ rev: 1.8.5
40
40
  hooks:
41
41
  - id: nbqa-black
42
42
  - id: nbqa-ruff
@@ -67,7 +67,7 @@ repos:
67
67
  - id: validate-cff
68
68
 
69
69
  - repo: https://github.com/abravalheri/validate-pyproject
70
- rev: v0.15
70
+ rev: v0.16
71
71
  hooks:
72
72
  - id: validate-pyproject
73
73
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cf_xarray
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -9,4 +9,6 @@ from .helpers import bounds_to_vertices, vertices_to_bounds # noqa
9
9
  from .options import set_options # noqa
10
10
  from .utils import _get_version
11
11
 
12
+ from . import geometry # noqa
13
+
12
14
  __version__ = _get_version()
@@ -0,0 +1 @@
1
+ __version__ = "0.9.2"
@@ -28,8 +28,10 @@ from xarray.core.weighted import Weighted
28
28
  from . import sgrid
29
29
  from .criteria import (
30
30
  _DSG_ROLES,
31
+ _GEOMETRY_TYPES,
31
32
  cf_role_criteria,
32
33
  coordinate_criteria,
34
+ geometry_var_criteria,
33
35
  grid_mapping_var_criteria,
34
36
  regex,
35
37
  )
@@ -39,6 +41,7 @@ from .formatting import (
39
41
  _format_data_vars,
40
42
  _format_dsg_roles,
41
43
  _format_flags,
44
+ _format_geometries,
42
45
  _format_sgrid,
43
46
  _maybe_panel,
44
47
  )
@@ -198,7 +201,9 @@ def _get_groupby_time_accessor(
198
201
 
199
202
 
200
203
  def _get_custom_criteria(
201
- obj: DataArray | Dataset, key: Hashable, criteria: Mapping | None = None
204
+ obj: DataArray | Dataset,
205
+ key: Hashable,
206
+ criteria: Iterable[Mapping] | Mapping | None = None,
202
207
  ) -> list[Hashable]:
203
208
  """
204
209
  Translate from axis, coord, or custom name to variable name.
@@ -227,18 +232,16 @@ def _get_custom_criteria(
227
232
  except ImportError:
228
233
  from re import match as regex_match # type: ignore[no-redef]
229
234
 
230
- if isinstance(obj, DataArray):
231
- obj = obj._to_temp_dataset()
232
- variables = obj._variables
233
-
234
235
  if criteria is None:
235
236
  if not OPTIONS["custom_criteria"]:
236
237
  return []
237
238
  criteria = OPTIONS["custom_criteria"]
238
239
 
239
- if criteria is not None:
240
- criteria_iter = always_iterable(criteria, allowed=(tuple, list, set))
240
+ if isinstance(obj, DataArray):
241
+ obj = obj._to_temp_dataset()
242
+ variables = obj._variables
241
243
 
244
+ criteria_iter = always_iterable(criteria, allowed=(tuple, list, set))
242
245
  criteria_map = ChainMap(*criteria_iter)
243
246
  results: set = set()
244
247
  if key in criteria_map:
@@ -367,6 +370,21 @@ def _get_measure(obj: DataArray | Dataset, key: str) -> list[str]:
367
370
  return list(results)
368
371
 
369
372
 
373
+ def _parse_related_geometry_vars(attrs: Mapping) -> tuple[Hashable]:
374
+ names = itertools.chain(
375
+ *[
376
+ attrs.get(attr, "").split(" ")
377
+ for attr in [
378
+ "interior_ring",
379
+ "node_coordinates",
380
+ "node_count",
381
+ "part_node_count",
382
+ ]
383
+ ]
384
+ )
385
+ return tuple(n for n in names if n)
386
+
387
+
370
388
  def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
371
389
  """
372
390
  Translate from key (either CF key or variable name) to its bounds' variable names.
@@ -470,8 +488,14 @@ def _get_all(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
470
488
  """
471
489
  all_mappers: tuple[Mapper] = (
472
490
  _get_custom_criteria,
473
- functools.partial(_get_custom_criteria, criteria=cf_role_criteria), # type: ignore[assignment]
474
- functools.partial(_get_custom_criteria, criteria=grid_mapping_var_criteria),
491
+ functools.partial(
492
+ _get_custom_criteria,
493
+ criteria=(
494
+ cf_role_criteria,
495
+ grid_mapping_var_criteria,
496
+ geometry_var_criteria,
497
+ ),
498
+ ), # type: ignore[assignment]
475
499
  _get_axis_coord,
476
500
  _get_measure,
477
501
  _get_grid_mapping_name,
@@ -721,8 +745,7 @@ def _getitem(
721
745
  accessor: CFAccessor,
722
746
  key: Hashable,
723
747
  skip: list[Literal["coords", "measures"]] | None = None,
724
- ) -> DataArray:
725
- ...
748
+ ) -> DataArray: ...
726
749
 
727
750
 
728
751
  @overload
@@ -730,8 +753,7 @@ def _getitem(
730
753
  accessor: CFAccessor,
731
754
  key: Iterable[Hashable],
732
755
  skip: list[Literal["coords", "measures"]] | None = None,
733
- ) -> Dataset:
734
- ...
756
+ ) -> Dataset: ...
735
757
 
736
758
 
737
759
  def _getitem(
@@ -823,6 +845,23 @@ def _getitem(
823
845
  successful[k] = bool(grid_mapping)
824
846
  if grid_mapping:
825
847
  varnames.extend(grid_mapping)
848
+ elif "geometries" not in skip and (k == "geometry" or k in _GEOMETRY_TYPES):
849
+ geometries = _get_all(obj, k)
850
+ if geometries and k in _GEOMETRY_TYPES:
851
+ new = itertools.chain(
852
+ _parse_related_geometry_vars(
853
+ ChainMap(obj[g].attrs, obj[g].encoding)
854
+ )
855
+ for g in geometries
856
+ )
857
+ geometries.extend(*new)
858
+ if len(geometries) > 1 and scalar_key:
859
+ raise ValueError(
860
+ f"CF geometries must be represented by an Xarray Dataset. To request a Dataset in return please pass `[{k!r}]` instead."
861
+ )
862
+ successful[k] = bool(geometries)
863
+ if geometries:
864
+ varnames.extend(geometries)
826
865
  elif k in custom_criteria or k in cf_role_criteria:
827
866
  names = _get_all(obj, k)
828
867
  check_results(names, k)
@@ -1561,8 +1600,7 @@ class CFAccessor:
1561
1600
  _format_flags(self, rich), title="Flag Variable", rich=rich
1562
1601
  )
1563
1602
 
1564
- roles = self.cf_roles
1565
- if roles:
1603
+ if roles := self.cf_roles:
1566
1604
  if any(role in roles for role in _DSG_ROLES):
1567
1605
  yield _maybe_panel(
1568
1606
  _format_dsg_roles(self, dims, rich),
@@ -1578,6 +1616,13 @@ class CFAccessor:
1578
1616
  rich=rich,
1579
1617
  )
1580
1618
 
1619
+ if self.geometries:
1620
+ yield _maybe_panel(
1621
+ _format_geometries(self, dims, rich),
1622
+ title="Geometries",
1623
+ rich=rich,
1624
+ )
1625
+
1581
1626
  yield _maybe_panel(
1582
1627
  _format_coordinates(self, dims, coords, rich),
1583
1628
  title="Coordinates",
@@ -1757,12 +1802,42 @@ class CFAccessor:
1757
1802
 
1758
1803
  vardict: dict[str, list[Hashable]] = {}
1759
1804
  for k, v in variables.items():
1760
- if "cf_role" in v.attrs:
1761
- role = v.attrs["cf_role"]
1805
+ attrs_or_encoding = ChainMap(v.attrs, v.encoding)
1806
+ if role := attrs_or_encoding.get("cf_role", None):
1762
1807
  vardict[role] = vardict.setdefault(role, []) + [k]
1763
1808
 
1764
1809
  return {role_: sort_maybe_hashable(v) for role_, v in vardict.items()}
1765
1810
 
1811
+ @property
1812
+ def geometries(self) -> dict[str, list[Hashable]]:
1813
+ """
1814
+ Mapping geometry type names to variable names.
1815
+
1816
+ Returns
1817
+ -------
1818
+ dict
1819
+ Dictionary mapping geometry names to variable names.
1820
+
1821
+ References
1822
+ ----------
1823
+ Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#coordinates-metadata
1824
+ """
1825
+ vardict: dict[str, list[Hashable]] = {}
1826
+
1827
+ if isinstance(self._obj, Dataset):
1828
+ variables = self._obj._variables
1829
+ elif isinstance(self._obj, DataArray):
1830
+ variables = {"_": self._obj._variable}
1831
+
1832
+ for v in variables.values():
1833
+ attrs_or_encoding = ChainMap(v.attrs, v.encoding)
1834
+ if geometry := attrs_or_encoding.get("geometry", None):
1835
+ gtype = self._obj[geometry].attrs["geometry_type"]
1836
+ vardict.setdefault(gtype, [])
1837
+ if geometry not in vardict[gtype]:
1838
+ vardict[gtype] += [geometry]
1839
+ return {type_: sort_maybe_hashable(v) for type_, v in vardict.items()}
1840
+
1766
1841
  def get_associated_variable_names(
1767
1842
  self, name: Hashable, skip_bounds: bool = False, error: bool = True
1768
1843
  ) -> dict[str, list[Hashable]]:
@@ -1797,15 +1872,15 @@ class CFAccessor:
1797
1872
  "bounds",
1798
1873
  "grid_mapping",
1799
1874
  "grid",
1875
+ "geometry",
1800
1876
  ]
1801
1877
 
1802
1878
  coords: dict[str, list[Hashable]] = {k: [] for k in keys}
1803
1879
  attrs_or_encoding = ChainMap(self._obj[name].attrs, self._obj[name].encoding)
1804
1880
 
1805
- coordinates = attrs_or_encoding.get("coordinates", None)
1806
1881
  # Handles case where the coordinates attribute is None
1807
1882
  # This is used to tell xarray to not write a coordinates attribute
1808
- if coordinates:
1883
+ if coordinates := attrs_or_encoding.get("coordinates", None):
1809
1884
  coords["coordinates"] = coordinates.split(" ")
1810
1885
 
1811
1886
  if "cell_measures" in attrs_or_encoding:
@@ -1824,27 +1899,32 @@ class CFAccessor:
1824
1899
  )
1825
1900
  coords["cell_measures"] = []
1826
1901
 
1827
- if (
1828
- isinstance(self._obj, Dataset)
1829
- and "ancillary_variables" in attrs_or_encoding
1902
+ if isinstance(self._obj, Dataset) and (
1903
+ anc := attrs_or_encoding.get("ancillary_variables", None)
1830
1904
  ):
1831
- coords["ancillary_variables"] = attrs_or_encoding[
1832
- "ancillary_variables"
1833
- ].split(" ")
1905
+ coords["ancillary_variables"] = anc.split(" ")
1834
1906
 
1835
1907
  if not skip_bounds:
1836
- if "bounds" in attrs_or_encoding:
1837
- coords["bounds"] = [attrs_or_encoding["bounds"]]
1908
+ if bounds := attrs_or_encoding.get("bounds", None):
1909
+ coords["bounds"] = [bounds]
1838
1910
  for dim in self._obj[name].dims:
1839
- dbounds = self._obj[dim].attrs.get("bounds", None)
1840
- if dbounds:
1911
+ if dbounds := self._obj[dim].attrs.get("bounds", None):
1841
1912
  coords["bounds"].append(dbounds)
1842
1913
 
1843
- if "grid" in attrs_or_encoding:
1844
- coords["grid"] = [attrs_or_encoding["grid"]]
1914
+ for attrname in ["grid", "grid_mapping"]:
1915
+ if maybe := attrs_or_encoding.get(attrname, None):
1916
+ coords[attrname] = [maybe]
1845
1917
 
1846
- if "grid_mapping" in attrs_or_encoding:
1847
- coords["grid_mapping"] = [attrs_or_encoding["grid_mapping"]]
1918
+ more: Sequence[Hashable] = ()
1919
+ if geometry_var := attrs_or_encoding.get("geometry", None):
1920
+ coords["geometry"] = [geometry_var]
1921
+ _attrs = ChainMap(
1922
+ self._obj[geometry_var].attrs, self._obj[geometry_var].encoding
1923
+ )
1924
+ more = _parse_related_geometry_vars(_attrs)
1925
+ elif "geometry_type" in attrs_or_encoding:
1926
+ more = _parse_related_geometry_vars(attrs_or_encoding)
1927
+ coords["geometry"].extend(more)
1848
1928
 
1849
1929
  allvars = itertools.chain(*coords.values())
1850
1930
  missing = set(allvars) - set(self._maybe_to_dataset()._variables)
@@ -2276,8 +2356,8 @@ class CFDatasetAccessor(CFAccessor):
2276
2356
  References
2277
2357
  ----------
2278
2358
  Please refer to the CF conventions document :
2279
- 1. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate
2280
- 2. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-v-coord.
2359
+ 1. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#parametric-vertical-coordinate
2360
+ 2. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#parametric-v-coord.
2281
2361
 
2282
2362
  Examples
2283
2363
  --------
@@ -2553,7 +2633,7 @@ class CFDatasetAccessor(CFAccessor):
2553
2633
 
2554
2634
  References
2555
2635
  ----------
2556
- Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-boundaries.
2636
+ Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#cell-boundaries.
2557
2637
  """
2558
2638
  if keys is None:
2559
2639
  coords = tuple(self.keys())
@@ -2762,8 +2842,8 @@ class CFDataArrayAccessor(CFAccessor):
2762
2842
  References
2763
2843
  ----------
2764
2844
  Please refer to the CF conventions document :
2765
- 1. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate
2766
- 2. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-v-coord.
2845
+ 1. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#parametric-vertical-coordinate
2846
+ 2. http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#parametric-v-coord.
2767
2847
 
2768
2848
  Examples
2769
2849
  --------
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Encoders and decoders for CF conventions not implemented by Xarray.
3
3
  """
4
+
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
  import xarray as xr
@@ -12,7 +12,10 @@ except ImportError:
12
12
  from collections.abc import Mapping, MutableMapping
13
13
  from typing import Any
14
14
 
15
+ #: CF Roles understood by cf-xarray
15
16
  _DSG_ROLES = ["timeseries_id", "profile_id", "trajectory_id"]
17
+ #: Geometry types understood by cf-xarray
18
+ _GEOMETRY_TYPES = ("line", "point", "polygon")
16
19
 
17
20
  cf_role_criteria: Mapping[str, Mapping[str, str]] = {
18
21
  k: {"cf_role": k}
@@ -31,6 +34,12 @@ grid_mapping_var_criteria: Mapping[str, Mapping[str, Any]] = {
31
34
  "grid_mapping": {"grid_mapping_name": re.compile(".")}
32
35
  }
33
36
 
37
+ # A geometry container is anything with a geometry_type attribute
38
+ geometry_var_criteria: Mapping[str, Mapping[str, Any]] = {
39
+ "geometry": {"geometry_type": re.compile(".")},
40
+ **{k: {"geometry_type": k} for k in _GEOMETRY_TYPES},
41
+ }
42
+
34
43
  coordinate_criteria: MutableMapping[str, MutableMapping[str, tuple]] = {
35
44
  "latitude": {
36
45
  "standard_name": ("latitude",),
@@ -748,3 +748,32 @@ sgrid_delft3["grid"] = xr.DataArray(
748
748
  node_coordinates="node_lon node_lat node_elevation",
749
749
  ),
750
750
  )
751
+
752
+
753
+ def point_dataset():
754
+ from shapely.geometry import MultiPoint, Point
755
+
756
+ da = xr.DataArray(
757
+ [
758
+ MultiPoint([(1.0, 2.0), (2.0, 3.0)]),
759
+ Point(3.0, 4.0),
760
+ Point(4.0, 5.0),
761
+ Point(3.0, 4.0),
762
+ ],
763
+ dims=("index",),
764
+ name="geometry",
765
+ )
766
+ ds = da.to_dataset()
767
+ return ds
768
+
769
+
770
+ def encoded_point_dataset():
771
+ from .geometry import encode_geometries
772
+
773
+ ds = encode_geometries(point_dataset())
774
+ ds["data"] = (
775
+ "index",
776
+ np.arange(ds.sizes["index"]),
777
+ {"geometry": "geometry_container"},
778
+ )
779
+ return ds
@@ -110,9 +110,11 @@ def _print_rows(subtitle: str, rows: list[str], rich: bool):
110
110
 
111
111
  # Add subtitle to the first row, align other rows
112
112
  rows = [
113
- _format_subtitle(subtitle, rich=rich) + row
114
- if i == 0
115
- else len(subtitle) * " " + row
113
+ (
114
+ _format_subtitle(subtitle, rich=rich) + row
115
+ if i == 0
116
+ else len(subtitle) * " " + row
117
+ )
116
118
  for i, row in enumerate(rows)
117
119
  ]
118
120
 
@@ -293,6 +295,17 @@ def _format_dsg_roles(accessor, dims, rich):
293
295
  )
294
296
 
295
297
 
298
+ def _format_geometries(accessor, dims, rich):
299
+ yield make_text_section(
300
+ accessor,
301
+ "CF Geometries",
302
+ "geometries",
303
+ dims=dims,
304
+ # valid_keys=_DSG_ROLES,
305
+ rich=rich,
306
+ )
307
+
308
+
296
309
  def _format_coordinates(accessor, dims, coords, rich):
297
310
  from .accessor import _AXIS_NAMES, _CELL_MEASURES, _COORD_NAMES
298
311