diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index 14d7314e..f0a84bdf 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -1317,7 +1317,7 @@ def rename_like( Variables will be renamed to match variable names in this xarray object skip: str, Iterable[str], optional Limit the renaming excluding - ("axes", "cell_measures", "coordinates", "standard_names") + ("axes", "bounds", cell_measures", "coordinates", "standard_names") or a subset thereof. Returns @@ -1332,24 +1332,48 @@ def rename_like( good_keys = ourkeys & theirkeys keydict = {} for key in good_keys: - ours = set(_get_all(self._obj, key)) - theirs = set(_get_all(other, key)) + ours = set(apply_mapper(_get_all, self._obj, key)) + theirs = set(apply_mapper(_get_all, other, key)) for attr in skip: - ours -= set(getattr(self, attr).get(key, [])) - theirs -= set(getattr(other.cf, attr).get(key, [])) + ours.difference_update(getattr(self, attr).get(key, [])) + theirs.difference_update(getattr(other.cf, attr).get(key, [])) if ours and theirs: keydict[key] = dict(ours=list(ours), theirs=list(theirs)) - conflicts = {} - for k0, v0 in keydict.items(): - if len(v0["ours"]) > 1 or len(v0["theirs"]) > 1: - conflicts[k0] = v0 - continue - for v1 in keydict.values(): - # Conflicts have same ours but different theirs or vice versa - if (v0["ours"] == v1["ours"]) != (v0["theirs"] == v1["theirs"]): + def get_renamer_and_conflicts(keydict): + conflicts = {} + for k0, v0 in keydict.items(): + if len(v0["ours"]) > 1 or len(v0["theirs"]) > 1: conflicts[k0] = v0 - break + continue + for v1 in keydict.values(): + # Conflicts have same ours but different theirs or vice versa + if (v0["ours"] == v1["ours"]) != (v0["theirs"] == v1["theirs"]): + conflicts[k0] = v0 + break + + renamer = { + v["ours"][0]: v["theirs"][0] + for k, v in keydict.items() + if k not in conflicts + } + + return renamer, conflicts + + # Run get_renamer_and_conflicts twice. + # The second time add the bounds associated with variables to rename + renamer, conflicts = get_renamer_and_conflicts(keydict) + if "bounds" not in skip: + for k, v in renamer.items(): + ours = set(getattr(self, "bounds", {}).get(k, [])) + theirs = set(getattr(other.cf, "bounds", {}).get(v, [])) + if ours and theirs: + ours.update(keydict.get(k, {}).get("ours", [])) + theirs.update(keydict.get(k, {}).get("theirs", [])) + keydict[k] = dict(ours=list(ours), theirs=list(theirs)) + renamer, conflicts = get_renamer_and_conflicts(keydict) + + # Rename and warn if conflicts: warnings.warn( "Conflicting variables skipped:\n" @@ -1363,23 +1387,28 @@ def rename_like( ), UserWarning, ) - - renamer = { - v["ours"][0]: v["theirs"][0] - for k, v in keydict.items() - if k not in conflicts - } newobj = self._obj.rename(renamer) - # rename variable names in the coordinates attribute + # rename variable names in the attributes # if present ds = self._maybe_to_dataset(newobj) for _, variable in ds.variables.items(): - coordinates = variable.attrs.get("coordinates", None) - if coordinates: - for k, v in renamer.items(): - coordinates = coordinates.replace(k, v) - variable.attrs["coordinates"] = coordinates + for attr in ("bounds", "coordinates", "cell_measures"): + if attr == "cell_measures": + varlist = [ + f"{k}: {renamer.get(v, v)}" + for k, v in parse_cell_methods_attr( + variable.attrs.get(attr, "") + ).items() + ] + else: + varlist = [ + renamer.get(var, var) + for var in variable.attrs.get(attr, "").split() + ] + + if varlist: + variable.attrs[attr] = " ".join(varlist) return self._maybe_to_dataarray(ds) def guess_coord_axis(self, verbose: bool = False) -> Union[DataArray, Dataset]: diff --git a/cf_xarray/tests/test_accessor.py b/cf_xarray/tests/test_accessor.py index e89a4a25..f2e65640 100644 --- a/cf_xarray/tests/test_accessor.py +++ b/cf_xarray/tests/test_accessor.py @@ -241,7 +241,7 @@ def test_rename_like(): for k in ["TLONG", "TLAT"]: assert k not in renamed.coords assert k in original.coords - assert original.TEMP.attrs["coordinates"] == "TLONG TLAT" + assert original.TEMP.attrs["coordinates"] == "TLONG TLAT" assert "lon" in renamed.coords assert "lat" in renamed.coords @@ -271,6 +271,20 @@ def test_rename_like(): actual = da.cf.rename_like(airds, skip="axes").cf.coordinates assert expected == actual + # rename bounds + original = airds.cf[["air"]].cf.add_bounds("lon") + other = popds.cf[["TEMP"]].cf.add_bounds("nlon") + renamed = original.cf.rename_like(other, skip="coordinates") + assert renamed.cf.bounds["nlon"] == ["nlon_bounds"] + + # rename cell measures + other = airds.cf["air"].cf.rename(area="CELL_AREA") + other.attrs["cell_measures"] = other.attrs["cell_measures"].replace( + "cell_area", "CELL_AREA" + ) + renamed = airds.cf["air"].cf.rename_like(other) + assert renamed.cf.cell_measures["area"] == ["CELL_AREA"] + @pytest.mark.parametrize("obj", objects) @pytest.mark.parametrize( diff --git a/doc/whats-new.rst b/doc/whats-new.rst index fb03011f..9f444669 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -7,6 +7,7 @@ v0.5.3 (unreleased) =================== - Begin adding support for units with a unit registry for pint arrays. :pr:`197`. By `Jon Thielen`_ and `Justus Magin`_. +- :py:meth:`Dataset.cf.rename_like` also updates the ``bounds`` and ``cell_measures`` attributes. By `Mattia Almansi`_. v0.5.2 (May 11, 2021) =====================