Skip to content

Commit

Permalink
Rewrite unit formatter for pint 0.24 and earlier (#523)
Browse files Browse the repository at this point in the history
* Rewrite unit formatter for pint 0.24

* add pins in pyproject

* Dimensionless as 1 - skip long test on previous pint

* Skip all dimensionless
  • Loading branch information
aulemahal authored Jul 5, 2024
1 parent 9f1ca50 commit a74efdb
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 54 deletions.
5 changes: 5 additions & 0 deletions cf_xarray/tests/test_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
97 changes: 46 additions & 51 deletions cf_xarray/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<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", "%")
@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.
Expand Down
4 changes: 2 additions & 2 deletions doc/units.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a74efdb

Please sign in to comment.