From 31251d68dbb0e5b2cf2addcf295934aee31158b4 Mon Sep 17 00:00:00 2001 From: malmans2 Date: Wed, 21 Apr 2021 13:08:46 +0100 Subject: [PATCH 1/5] add bounds property --- cf_xarray/accessor.py | 55 ++++++++++++++++++++++++++++---- cf_xarray/tests/test_accessor.py | 30 +++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index e22e0019..70b48683 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -255,6 +255,31 @@ def _get_measure(obj: Union[DataArray, Dataset], key: str) -> List[str]: return list(results) +def _get_bounds(obj: Union[DataArray, Dataset], key: str) -> List[str]: + """ + Translate from key (either CF key or variable name) to appropriate bounds names. + This function interprets the ``bounds`` attribute on DataArrays. + + Parameters + ---------- + obj : DataArray, Dataset + DataArray belonging to the coordinate to be checked + key : str + key to check for. + + Returns + ------- + List[str], Variable name(s) in parent xarray object that matches axis or coordinate `key` + """ + + results = [] + for var in apply_mapper(_get_all, obj, key, error=False, default=[key]): + if "bounds" in obj[var].attrs: + results += [obj[var].attrs["bounds"]] + + return results + + def _get_with_standard_name( obj: Union[DataArray, Dataset], name: Union[str, List[str]] ) -> List[str]: @@ -1027,6 +1052,7 @@ def make_text_section(subtitle, vardict, valid_values, default_keys=None): "Cell Measures", self.cell_measures, coords, _CELL_MEASURES ) text += make_text_section("Standard Names", self.standard_names, coords) + text += make_text_section("Bounds", self.bounds, coords) if isinstance(self._obj, Dataset): data_vars = self._obj.data_vars text += "\nData Variables:" @@ -1034,6 +1060,7 @@ def make_text_section(subtitle, vardict, valid_values, default_keys=None): "Cell Measures", self.cell_measures, data_vars, _CELL_MEASURES ) text += make_text_section("Standard Names", self.standard_names, data_vars) + text += make_text_section("Bounds", self.bounds, data_vars) return text @@ -1132,6 +1159,26 @@ def cell_measures(self) -> Dict[str, List[str]]: return {k: sorted(set(v)) for k, v in measures.items() if v} + @property + def bounds(self) -> Dict[str, List[str]]: + """ + Property that returns a dictionary mapping valid keys to variable names of their bounds. + + Returns + ------- + Dictionary mapping valid keys to variable names of their bounds. + """ + + obj = self._obj + keys = self.keys() + keys |= set(obj.variables if isinstance(obj, Dataset) else obj.coords) + + vardict = { + key: apply_mapper(_get_bounds, obj, key, error=False) for key in keys + } + + return {k: sorted(v) for k, v in vardict.items() if v} + def get_standard_names(self) -> List[str]: warnings.warn( @@ -1493,12 +1540,8 @@ def get_bounds(self, key: str) -> DataArray: ------- DataArray """ - name = apply_mapper( - _single(_get_all), self._obj, key, error=False, default=[key] - )[0] - bounds = self._obj[name].attrs["bounds"] - obj = self._maybe_to_dataset() - return obj[bounds] + + return apply_mapper(_variables(_single(_get_bounds)), self._obj, key)[0] def get_bounds_dim_name(self, key: str) -> str: """ diff --git a/cf_xarray/tests/test_accessor.py b/cf_xarray/tests/test_accessor.py index 77253114..42d99656 100644 --- a/cf_xarray/tests/test_accessor.py +++ b/cf_xarray/tests/test_accessor.py @@ -61,10 +61,14 @@ def test_repr(): * longitude: ['lon'] * time: ['time'] + - Bounds: n/a + Data Variables: - Cell Measures: area, volume: n/a - Standard Names: air_temperature: ['air'] + + - Bounds: n/a """ assert actual == dedent(expected) @@ -89,6 +93,8 @@ def test_repr(): - Standard Names: * latitude: ['lat'] * longitude: ['lon'] * time: ['time'] + + - Bounds: n/a """ assert actual == dedent(expected) @@ -108,11 +114,15 @@ def test_repr(): - Standard Names: n/a + - Bounds: n/a + Data Variables: - Cell Measures: area, volume: n/a - Standard Names: sea_water_potential_temperature: ['TEMP'] sea_water_x_velocity: ['UVEL'] + + - Bounds: n/a """ assert actual == dedent(expected) @@ -163,6 +173,8 @@ def test_cell_measures(): - Standard Names: air_temperature: ['air'] foo_std_name: ['foo'] + + - Bounds: n/a """ assert actual.endswith(dedent(expected)) @@ -625,6 +637,11 @@ def test_add_bounds(obj, dims): def test_bounds(): ds = airds.copy(deep=True).cf.add_bounds("lat") + + actual = ds.cf.bounds + expected = {"Y": ["lat_bounds"], "lat": ["lat_bounds"], "latitude": ["lat_bounds"]} + assert ds.cf.bounds == ds.cf["air"].cf.bounds == expected + actual = ds.cf[["lat"]] expected = ds[["lat", "lat_bounds"]] assert_identical(actual, expected) @@ -651,6 +668,19 @@ def test_bounds(): with pytest.warns(UserWarning, match="{'foo'} not found in object"): ds.cf[["air"]] + # Dataset has bounds + expected = """\ + - Bounds: Y: ['lat_bounds'] + lat: ['lat_bounds'] + latitude: ['lat_bounds'] + """ + assert dedent(expected) in ds.cf.__repr__() + + # DataArray does not have bounds + expected = airds.cf["air"].cf.__repr__() + actual = ds.cf["air"].cf.__repr__() + assert actual == expected + def test_bounds_to_vertices(): # All available From 2f701c32e9bbdc14c34e82adb9c82b063acb926e Mon Sep 17 00:00:00 2001 From: malmans2 Date: Wed, 21 Apr 2021 18:02:59 +0100 Subject: [PATCH 2/5] fix docs and avoid duplicates in bounds lists --- cf_xarray/accessor.py | 19 ++++++++++--------- doc/api.rst | 2 ++ doc/whats-new.rst | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index 70b48683..48969b2d 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -257,7 +257,7 @@ def _get_measure(obj: Union[DataArray, Dataset], key: str) -> List[str]: def _get_bounds(obj: Union[DataArray, Dataset], key: str) -> List[str]: """ - Translate from key (either CF key or variable name) to appropriate bounds names. + Translate from key (either CF key or variable name) to its bounds' variable names. This function interprets the ``bounds`` attribute on DataArrays. Parameters @@ -269,15 +269,15 @@ def _get_bounds(obj: Union[DataArray, Dataset], key: str) -> List[str]: Returns ------- - List[str], Variable name(s) in parent xarray object that matches axis or coordinate `key` + List[str], Variable name(s) in parent xarray object that are bounds of `key` """ - results = [] + results = set() for var in apply_mapper(_get_all, obj, key, error=False, default=[key]): if "bounds" in obj[var].attrs: - results += [obj[var].attrs["bounds"]] + results |= {obj[var].attrs["bounds"]} - return results + return list(results) def _get_with_standard_name( @@ -1162,11 +1162,12 @@ def cell_measures(self) -> Dict[str, List[str]]: @property def bounds(self) -> Dict[str, List[str]]: """ - Property that returns a dictionary mapping valid keys to variable names of their bounds. + Property that returns a dictionary mapping valid keys + to the variable names of their bounds. Returns ------- - Dictionary mapping valid keys to variable names of their bounds. + Dictionary mapping valid keys to the variable names of their bounds. """ obj = self._obj @@ -1191,7 +1192,7 @@ def get_standard_names(self) -> List[str]: @property def standard_names(self) -> Dict[str, List[str]]: """ - Returns a sorted list of standard names in Dataset. + Returns a dictionary mapping standard names to variable names. Parameters ---------- @@ -1200,7 +1201,7 @@ def standard_names(self) -> Dict[str, List[str]]: Returns ------- - Dictionary of standard names in dataset + Dictionary mapping standard names to variable names. """ if isinstance(self._obj, Dataset): variables = self._obj.variables diff --git a/doc/api.rst b/doc/api.rst index 4c987f51..b71a2d26 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -28,6 +28,7 @@ Attributes :template: autosummary/accessor_attribute.rst DataArray.cf.axes + DataArray.cf.bounds DataArray.cf.cell_measures DataArray.cf.coordinates DataArray.cf.standard_names @@ -63,6 +64,7 @@ Attributes :template: autosummary/accessor_attribute.rst Dataset.cf.axes + Dataset.cf.bounds Dataset.cf.cell_measures Dataset.cf.coordinates Dataset.cf.standard_names diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 1760fc56..3a54cbd1 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -6,6 +6,7 @@ What's New v0.5.2 (unreleased) =================== +- Added :py:attr:`Dataset.cf.axes` to return a dictionary mapping valid keys to the variable names of their bounds. By `Mattia Almansi`_. - :py:meth:`DataArray.cf.differentiate` and :py:meth:`Dataset.cf.differentiate` can optionally correct sign of the derivative by interpreting the ``"positive"`` attribute. By `Deepak Cherian`_. From aef6f49a32b04a72a64d471c415270fa7cffe133 Mon Sep 17 00:00:00 2001 From: malmans2 Date: Thu, 22 Apr 2021 11:32:16 +0100 Subject: [PATCH 3/5] cf.bounds only for Datasets --- cf_xarray/accessor.py | 72 ++++++++++++++++++-------------- cf_xarray/tests/test_accessor.py | 2 +- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index 48969b2d..cfe5934e 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -461,6 +461,14 @@ def _getattr( try: attribute: Union[Mapping, Callable] = getattr(obj, attr) except AttributeError: + if getattr( + CFDatasetAccessor if isinstance(obj, DataArray) else CFDataArrayAccessor, + attr, + None, + ): + raise AttributeError( + f"{obj.__class__.__name__+'.cf'!r} object has no attribute {attr!r}" + ) raise AttributeError( f"{attr!r} is not a valid attribute on the underlying xarray object." ) @@ -1001,7 +1009,9 @@ def __repr__(self): coords = self._obj.coords dims = self._obj.dims - def make_text_section(subtitle, vardict, valid_values, default_keys=None): + def make_text_section(subtitle, attr, valid_values, default_keys=None): + + vardict = getattr(self, attr, {}) star = " * " tab = len(star) * " " @@ -1044,23 +1054,21 @@ def make_text_section(subtitle, vardict, valid_values, default_keys=None): return "\n".join(rows) + "\n" text = "Coordinates:" - text += make_text_section("CF Axes", self.axes, coords, _AXIS_NAMES) + text += make_text_section("CF Axes", "axes", coords, _AXIS_NAMES) + text += make_text_section("CF Coordinates", "coordinates", coords, _COORD_NAMES) text += make_text_section( - "CF Coordinates", self.coordinates, coords, _COORD_NAMES + "Cell Measures", "cell_measures", coords, _CELL_MEASURES ) - text += make_text_section( - "Cell Measures", self.cell_measures, coords, _CELL_MEASURES - ) - text += make_text_section("Standard Names", self.standard_names, coords) - text += make_text_section("Bounds", self.bounds, coords) + text += make_text_section("Standard Names", "standard_names", coords) + text += make_text_section("Bounds", "bounds", coords) if isinstance(self._obj, Dataset): data_vars = self._obj.data_vars text += "\nData Variables:" text += make_text_section( - "Cell Measures", self.cell_measures, data_vars, _CELL_MEASURES + "Cell Measures", "cell_measures", data_vars, _CELL_MEASURES ) - text += make_text_section("Standard Names", self.standard_names, data_vars) - text += make_text_section("Bounds", self.bounds, data_vars) + text += make_text_section("Standard Names", "standard_names", data_vars) + text += make_text_section("Bounds", "bounds", data_vars) return text @@ -1159,27 +1167,6 @@ def cell_measures(self) -> Dict[str, List[str]]: return {k: sorted(set(v)) for k, v in measures.items() if v} - @property - def bounds(self) -> Dict[str, List[str]]: - """ - Property that returns a dictionary mapping valid keys - to the variable names of their bounds. - - Returns - ------- - Dictionary mapping valid keys to the variable names of their bounds. - """ - - obj = self._obj - keys = self.keys() - keys |= set(obj.variables if isinstance(obj, Dataset) else obj.coords) - - vardict = { - key: apply_mapper(_get_bounds, obj, key, error=False) for key in keys - } - - return {k: sorted(v) for k, v in vardict.items() if v} - def get_standard_names(self) -> List[str]: warnings.warn( @@ -1528,6 +1515,27 @@ def __getitem__(self, key: Union[str, List[str]]) -> Union[DataArray, Dataset]: """ return _getitem(self, key) + @property + def bounds(self) -> Dict[str, List[str]]: + """ + Property that returns a dictionary mapping valid keys + to the variable names of their bounds. + + Returns + ------- + Dictionary mapping valid keys to the variable names of their bounds. + """ + + obj = self._obj + keys = self.keys() + keys |= set(obj.variables if isinstance(obj, Dataset) else obj.coords) + + vardict = { + key: apply_mapper(_get_bounds, obj, key, error=False) for key in keys + } + + return {k: sorted(v) for k, v in vardict.items() if v} + def get_bounds(self, key: str) -> DataArray: """ Get bounds variable corresponding to key. diff --git a/cf_xarray/tests/test_accessor.py b/cf_xarray/tests/test_accessor.py index 42d99656..41bbb9f8 100644 --- a/cf_xarray/tests/test_accessor.py +++ b/cf_xarray/tests/test_accessor.py @@ -640,7 +640,7 @@ def test_bounds(): actual = ds.cf.bounds expected = {"Y": ["lat_bounds"], "lat": ["lat_bounds"], "latitude": ["lat_bounds"]} - assert ds.cf.bounds == ds.cf["air"].cf.bounds == expected + assert ds.cf.bounds == expected actual = ds.cf[["lat"]] expected = ds[["lat", "lat_bounds"]] From 4276f7e82f40185052de55b0153d8d31f259a7cc Mon Sep 17 00:00:00 2001 From: malmans2 Date: Thu, 22 Apr 2021 11:34:44 +0100 Subject: [PATCH 4/5] fix api --- doc/api.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api.rst b/doc/api.rst index b71a2d26..49aa425c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -28,7 +28,6 @@ Attributes :template: autosummary/accessor_attribute.rst DataArray.cf.axes - DataArray.cf.bounds DataArray.cf.cell_measures DataArray.cf.coordinates DataArray.cf.standard_names From bec361b4e8d30a1f594f7677d056361953acb8d8 Mon Sep 17 00:00:00 2001 From: malmans2 Date: Thu, 22 Apr 2021 11:49:35 +0100 Subject: [PATCH 5/5] remove useless if --- cf_xarray/accessor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index cfe5934e..a49761fb 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -1527,8 +1527,7 @@ def bounds(self) -> Dict[str, List[str]]: """ obj = self._obj - keys = self.keys() - keys |= set(obj.variables if isinstance(obj, Dataset) else obj.coords) + keys = self.keys() | set(obj.variables) vardict = { key: apply_mapper(_get_bounds, obj, key, error=False) for key in keys