Skip to content

Commit a74efdb

Browse files
authored
Rewrite unit formatter for pint 0.24 and earlier (#523)
* Rewrite unit formatter for pint 0.24 * add pins in pyproject * Dimensionless as 1 - skip long test on previous pint * Skip all dimensionless
1 parent 9f1ca50 commit a74efdb

File tree

4 files changed

+54
-54
lines changed

4 files changed

+54
-54
lines changed

cf_xarray/tests/test_units.py

+5
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,16 @@ def test_udunits_power_syntax_parse_units():
7575
("m ** -1", "m-1"),
7676
("m ** 2 / s ** 2", "m2 s-2"),
7777
("m ** 3 / (kg * s ** 2)", "m3 kg-1 s-2"),
78+
("", "1"),
7879
),
7980
)
8081
def test_udunits_format(units, expected):
8182
u = ureg.parse_units(units)
83+
if units == "":
84+
# The non-shortened dimensionless can only work with recent pint
85+
pytest.importorskip("pint", minversion="0.24.1")
8286

87+
assert f"{u:~cf}" == expected
8388
assert f"{u:cf}" == expected
8489

8590

cf_xarray/units.py

+46-51
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,57 @@
44
import re
55

66
import pint
7-
from pint import ( # noqa: F401
8-
DimensionalityError,
9-
UndefinedUnitError,
10-
UnitStrippedWarning,
11-
)
7+
from packaging.version import Version
128

139
from .utils import emit_user_level_warning
1410

15-
# from `xclim`'s unit support module with permission of the maintainers
16-
try:
1711

18-
@pint.register_unit_format("cf")
19-
def short_formatter(unit, registry, **options):
20-
"""Return a CF-compliant unit string from a `pint` unit.
21-
22-
Parameters
23-
----------
24-
unit : pint.UnitContainer
25-
Input unit.
26-
registry : pint.UnitRegistry
27-
the associated registry
28-
**options
29-
Additional options (may be ignored)
30-
31-
Returns
32-
-------
33-
out : str
34-
Units following CF-Convention, using symbols.
35-
"""
36-
import re
37-
38-
# convert UnitContainer back to Unit
39-
unit = registry.Unit(unit)
40-
# Print units using abbreviations (millimeter -> mm)
41-
s = f"{unit:~D}"
42-
43-
# Search and replace patterns
44-
pat = r"(?P<inverse>(?:1 )?/ )?(?P<unit>\w+)(?: \*\* (?P<pow>\d))?"
45-
46-
def repl(m):
47-
i, u, p = m.groups()
48-
p = p or (1 if i else "")
49-
neg = "-" if i else ""
50-
51-
return f"{u}{neg}{p}"
52-
53-
out, n = re.subn(pat, repl, s)
54-
55-
# Remove multiplications
56-
out = out.replace(" * ", " ")
57-
# Delta degrees:
58-
out = out.replace("Δ°", "delta_deg")
59-
return out.replace("percent", "%")
12+
@pint.register_unit_format("cf")
13+
def short_formatter(unit, registry, **options):
14+
"""Return a CF-compliant unit string from a `pint` unit.
15+
16+
Parameters
17+
----------
18+
unit : pint.UnitContainer
19+
Input unit.
20+
registry : pint.UnitRegistry
21+
The associated registry
22+
**options
23+
Additional options (may be ignored)
24+
25+
Returns
26+
-------
27+
out : str
28+
Units following CF-Convention, using symbols.
29+
"""
30+
# pint 0.24.1 gives {"dimensionless": 1} for non-shortened dimensionless units
31+
# CF uses "1" to denote fractions and dimensionless quantities
32+
if unit == {"dimensionless": 1} or not unit:
33+
return "1"
34+
35+
# If u is a name, get its symbol (same as pint's "~" pre-formatter)
36+
# otherwise, assume a symbol (pint should have already raised on invalid units before this)
37+
unit = pint.util.UnitsContainer(
38+
{
39+
registry._get_symbol(u) if u in registry._units else u: exp
40+
for u, exp in unit.items()
41+
}
42+
)
43+
44+
# Change in formatter signature in pint 0.24
45+
if Version(pint.__version__) < Version("0.24"):
46+
args = (unit.items(),)
47+
else:
48+
# Numerators splitted from denominators
49+
args = (
50+
((u, e) for u, e in unit.items() if e >= 0),
51+
((u, e) for u, e in unit.items() if e < 0),
52+
)
53+
54+
out = pint.formatter(*args, as_ratio=False, product_fmt=" ", power_fmt="{}{}")
55+
# To avoid potentiel unicode problems in netCDF. In both cases, this unit is not recognized by udunits
56+
return out.replace("Δ°", "delta_deg")
6057

61-
except ImportError:
62-
pass
6358

6459
# ------
6560
# Reused with modification from MetPy under the terms of the BSD 3-Clause License.

doc/units.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ hide-toc: true
1616

1717
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).
1818

19-
`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.
19+
`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.
2020

2121
## Formatting units
2222

@@ -27,5 +27,5 @@ from pint import application_registry as ureg
2727
import cf_xarray.units
2828
2929
u = ureg.Unit("m ** 3 / s ** 2")
30-
f"{u:~cf}"
30+
f"{u:cf}" # or {u:~cf}, both return the same short format
3131
```

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies = [
2222
dynamic = ["version"]
2323

2424
[project.optional-dependencies]
25-
all = ["matplotlib", "pint", "shapely", "regex", "rich", "pooch"]
25+
all = ["matplotlib", "pint >=0.18, !=0.24.0", "shapely", "regex", "rich", "pooch"]
2626

2727
[project.urls]
2828
homepage = "https://cf-xarray.readthedocs.io"

0 commit comments

Comments
 (0)