Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite unit formatter for pint 0.24 and earlier #523

Merged
merged 5 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading