From b2c9d3db3b6999bc9cecad11705d1131fe2c0406 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20M=C3=BCller?= <mueller.seb@posteo.de>
Date: Mon, 1 Jul 2024 23:52:56 +0200
Subject: [PATCH] copy fix for cf_units from
 https://github.com/xarray-contrib/cf-xarray/pull/523

---
 src/finam/data/cf_units.py | 103 ++++++++++++++++++++-----------------
 1 file changed, 56 insertions(+), 47 deletions(-)

diff --git a/src/finam/data/cf_units.py b/src/finam/data/cf_units.py
index a10f67b5..16f9c13b 100644
--- a/src/finam/data/cf_units.py
+++ b/src/finam/data/cf_units.py
@@ -3,60 +3,60 @@
 Source: cf-xarray (https://github.com/xarray-contrib/cf-xarray/blob/main/cf_xarray/units.py)
 License: Apache License 2.0 (https://github.com/xarray-contrib/cf-xarray/blob/main/LICENSE)
 """
+
 import functools
 import re
 import warnings
 
 import pint
+from packaging.version import Version
+
+
+@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 this for dimensionless units
+    if unit == {"dimensionless": 1}:
+        return ""
+
+    # 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()
+        }
+    )
 
-# pylint: disable-next=unused-import
-from pint import DimensionalityError, UndefinedUnitError, UnitStrippedWarning
-
-# from `xclim`'s unit support module with permission of the maintainers
-try:
+    # 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),
+        )
 
-    @pint.register_unit_format("cf")  # pylint: disable-next=unused-argument
-    def short_formatter(unit, registry, **options):
-        """Return a CF-compliant unit string from a :mod:`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.
-        """
-
-        # 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<inverse>(?:1 )?/ )?(?P<unit>\w+)(?: \*\* (?P<pow>\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", "%")
+    out = pint.formatter(*args, as_ratio=False, product_fmt=" ", power_fmt="{}{}")
+    # To avoid potentiel unicode problems in netCDF. In both case, 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.
@@ -75,7 +75,13 @@ units = pint.UnitRegistry(
     ],
     force_ndarray_like=True,
 )
+# ----- end block copied from metpy
 
+# need to insert to make sure this is the first preprocessor
+# This ensures we convert integer `1` to string `"1"`, as needed by pint.
+units.preprocessors.insert(0, str)
+
+# -----
 units.define("percent = 0.01 = %")
 
 # Define commonly encountered units (both CF and non-CF) not defined by pint
@@ -98,6 +104,8 @@ units.define(
 units.define(
     "degrees_east = degree = degrees_east = degrees_E = degreesE = degree_east = degree_E = degreeE"
 )
+# degrees for grid_longitude / grid_latitude for grid_mappings
+units.define("degrees = degree = degrees")
 units.define("[speed] = [length] / [time]")
 # ----- end block copied from xclim
 
@@ -110,7 +118,8 @@ try:
 except ImportError:
     warnings.warn(
         "Import(s) unavailable to set up matplotlib support...skipping this portion "
-        "of the setup."
+        "of the setup.",
+        UserWarning,
     )
 # end of vendored code from MetPy
 
-- 
GitLab