diff --git a/cf_xarray/tests/test_units.py b/cf_xarray/tests/test_units.py index 105fbb25..36c677cd 100644 --- a/cf_xarray/tests/test_units.py +++ b/cf_xarray/tests/test_units.py @@ -75,11 +75,16 @@ def test_udunits_power_syntax_parse_units(): ("m ** -1", "m-1"), ("m ** 2 / s ** 2", "m2 s-2"), ("m ** 3 / (kg * s ** 2)", "m3 kg-1 s-2"), + ("", "1"), ), ) def test_udunits_format(units, expected): u = ureg.parse_units(units) + if units == "": + # The non-shortened dimensionless can only work with recent pint + pytest.importorskip("pint", minversion="0.24.1") + assert f"{u:~cf}" == expected assert f"{u:cf}" == expected diff --git a/cf_xarray/units.py b/cf_xarray/units.py index 1ec75cbb..e1d553e8 100644 --- a/cf_xarray/units.py +++ b/cf_xarray/units.py @@ -4,62 +4,57 @@ import re import pint -from pint import ( # noqa: F401 - DimensionalityError, - UndefinedUnitError, - UnitStrippedWarning, -) +from packaging.version import Version from .utils import emit_user_level_warning -# from `xclim`'s unit support module with permission of the maintainers -try: - @pint.register_unit_format("cf") - def short_formatter(unit, registry, **options): - """Return a CF-compliant unit string from a `pint` unit. - - Parameters - ---------- - unit : pint.UnitContainer - Input unit. - registry : pint.UnitRegistry - the associated registry - **options - Additional options (may be ignored) - - Returns - ------- - out : str - Units following CF-Convention, using symbols. - """ - import re - - # convert UnitContainer back to Unit - unit = registry.Unit(unit) - # Print units using abbreviations (millimeter -> mm) - s = f"{unit:~D}" - - # Search and replace patterns - pat = r"(?P(?:1 )?/ )?(?P\w+)(?: \*\* (?P\d))?" - - def repl(m): - i, u, p = m.groups() - p = p or (1 if i else "") - neg = "-" if i else "" - - return f"{u}{neg}{p}" - - out, n = re.subn(pat, repl, s) - - # Remove multiplications - out = out.replace(" * ", " ") - # Delta degrees: - out = out.replace("Δ°", "delta_deg") - return out.replace("percent", "%") +@pint.register_unit_format("cf") +def short_formatter(unit, registry, **options): + """Return a CF-compliant unit string from a `pint` unit. + + Parameters + ---------- + unit : pint.UnitContainer + Input unit. + registry : pint.UnitRegistry + The associated registry + **options + Additional options (may be ignored) + + Returns + ------- + out : str + Units following CF-Convention, using symbols. + """ + # pint 0.24.1 gives {"dimensionless": 1} for non-shortened dimensionless units + # CF uses "1" to denote fractions and dimensionless quantities + if unit == {"dimensionless": 1} or not unit: + return "1" + + # If u is a name, get its symbol (same as pint's "~" pre-formatter) + # otherwise, assume a symbol (pint should have already raised on invalid units before this) + unit = pint.util.UnitsContainer( + { + registry._get_symbol(u) if u in registry._units else u: exp + for u, exp in unit.items() + } + ) + + # Change in formatter signature in pint 0.24 + if Version(pint.__version__) < Version("0.24"): + args = (unit.items(),) + else: + # Numerators splitted from denominators + args = ( + ((u, e) for u, e in unit.items() if e >= 0), + ((u, e) for u, e in unit.items() if e < 0), + ) + + out = pint.formatter(*args, as_ratio=False, product_fmt=" ", power_fmt="{}{}") + # To avoid potentiel unicode problems in netCDF. In both cases, this unit is not recognized by udunits + return out.replace("Δ°", "delta_deg") -except ImportError: - pass # ------ # Reused with modification from MetPy under the terms of the BSD 3-Clause License. diff --git a/doc/units.md b/doc/units.md index 4b743358..884828fd 100644 --- a/doc/units.md +++ b/doc/units.md @@ -16,7 +16,7 @@ hide-toc: true The xarray ecosystem supports unit-aware arrays using [pint](https://pint.readthedocs.io) and [pint-xarray](https://pint-xarray.readthedocs.io). Some changes are required to make these packages work well with [UDUNITS format recommended by the CF conventions](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units). -`cf_xarray` makes those recommended changes when you `import cf_xarray.units`. These changes allow pint to parse and format UDUNIT units strings, and add several custom units like `degrees_north` for latitude, `psu` for ocean salinity, etc. +`cf_xarray` makes those recommended changes when you `import cf_xarray.units`. These changes allow pint to parse and format UDUNIT units strings, and add several custom units like `degrees_north` for latitude, `psu` for ocean salinity, etc. Be aware that pint supports some units that UDUNITS does not recognize but `cf-xarray` will not try to detect them and raise an error. For example, a temperature subtraction returns "delta_degC" units in pint, which does not exist in UDUNITS. ## Formatting units @@ -27,5 +27,5 @@ from pint import application_registry as ureg import cf_xarray.units u = ureg.Unit("m ** 3 / s ** 2") -f"{u:~cf}" +f"{u:cf}" # or {u:~cf}, both return the same short format ``` diff --git a/pyproject.toml b/pyproject.toml index 073b9777..8eccfc66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -all = ["matplotlib", "pint", "shapely", "regex", "rich", "pooch"] +all = ["matplotlib", "pint >=0.18, !=0.24.0", "shapely", "regex", "rich", "pooch"] [project.urls] homepage = "https://cf-xarray.readthedocs.io"