diff --git a/HISTORY.rst b/HISTORY.rst index 77c40817c..e86099a27 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -35,6 +35,7 @@ Bug fixes ^^^^^^^^^ * The docstring of ``cool_night_index`` suggested that `lat` was an optional parameter. This has been corrected. (:issue:`1179`, :pull:`1180`). * The ``mean_radiant_temperature`` indice was accessing hardcoded `lat` and `lon` coordinates from passed DataArrays. This now uses `cf-xarray` accessors. (:pull:`1180`). +* Adopt (and adapt) unit registry declaration and preprocessors from ``cf-xarray`` to circumvent bugs caused by a refactor in ``pint`` 0.20. It also cleans the code a little bit. (:issue:`1211`, :pull:`1212`). Internal changes ^^^^^^^^^^^^^^^^ diff --git a/xclim/core/units.py b/xclim/core/units.py index eb9cc458f..2d79122ec 100644 --- a/xclim/core/units.py +++ b/xclim/core/units.py @@ -8,17 +8,15 @@ """ from __future__ import annotations +import functools import re import warnings from inspect import signature from typing import Any, Callable -import pint.converters -import pint.unit +import pint import xarray as xr from boltons.funcutils import wraps -from pint import Unit -from pint.definitions import UnitDefinition from .calendar import date_range, get_calendar, parse_offset from .options import datacheck @@ -41,12 +39,22 @@ ] -units = pint.UnitRegistry(autoconvert_offset_to_baseunit=True, on_redefinition="ignore") -units.define( - pint.unit.UnitDefinition( - "percent", "%", ("pct",), pint.converters.ScaleConverter(0.01) - ) +# shamelessly adapted from `cf-xarray` (which adopted it from MetPy and xclim itself) +units = pint.UnitRegistry( + autoconvert_offset_to_baseunit=True, + preprocessors=[ + functools.partial( + re.compile( + r"(?<=[A-Za-z])(?![A-Za-z])(? Unit: +def units2pint(value: xr.DataArray | str | units.Quantity) -> pint.Unit: """Return the pint Unit for the DataArray units. Parameters @@ -120,17 +128,9 @@ def units2pint(value: xr.DataArray | str | units.Quantity) -> Unit: Returns ------- - pint.unit.UnitDefinition + pint.Unit Units of the data array. """ - - def _transform(s): - """Convert a CF-unit string to a pint expression.""" - if s == "%": - return "percent" - - return re.subn(r"([a-zA-Z]+)\^?(-?\d)", r"\g<1>**\g<2>", s)[0] - if isinstance(value, str): unit = value elif isinstance(value, xr.DataArray): @@ -163,19 +163,11 @@ def _transform(s): "Remove white space from temperature units, e.g. use `degC`." ) - try: # Pint compatible - return units.parse_units(unit) - except ( - pint.UndefinedUnitError, - pint.DimensionalityError, - AttributeError, - TypeError, - ): # Convert from CF-units to pint-compatible - return units.parse_units(_transform(unit)) + return units.parse_units(unit) # Note: The pint library does not have a generic Unit or Quantity type at the moment. Using "Any" as a stand-in. -def pint2cfunits(value: UnitDefinition) -> str: +def pint2cfunits(value: pint.Unit) -> str: """Return a CF-compliant unit string from a `pint` unit. Parameters @@ -210,7 +202,8 @@ def repl(m): out = out.replace(" * ", " ") # Delta degrees: out = out.replace("Δ°", "delta_deg") - return out.replace("percent", "%") + # Percents + return out.replace("percent", "%").replace("pct", "%") def ensure_cf_units(ustr: str) -> str: