Closed aulemahal closed 4 months ago
I think I found the error source, but I don't know how to fix.
The custom formatter is called by NewFormatter
, a class declared in the wrapper itself. This class has the format_unit
method which prepares the unit with pint.delegates.formatter._compound_unit_helpers.prepare_compount_unit
. In the case of a dimensionless quantity, that last function has a conditional I don't understand:
So with a spec without a "~", the first output of prepare_compount_unit
is [("dimensionless", 1)]
. NewFormatter
goes on and wraps this into a UnitsContainer
: https://github.com/hgrecco/pint/blob/ad02b8718cedb8ef99437855ee718808f2513af3/pint/delegates/formatter/_to_register.py#L102
The resulting container prints (repr) as <UnitsContainer({'dimensionless': 1})>
. However, if we go back to the initial unit, its container rather prints as repr(u._units)
<UnitsContainer({})>
. I thus assume the first UnitsContainer, the one sent by NewFormatter
to the custom function, is wrong.
Adding return ([("dimensionless", 1)], [])
was the most obvious way I saw to get the formatters to return "dimensionless" as the longform. I'm not sure what the previous behaviour was.
Why do you need to create a unit inside your custom formatter?
Because the custom formatter builds on the default formatter. For calling the default one it needs a Unit object.
But I guess one solution would be to copy the formatter
call from the default formatter instead of actually calling it. Hopefully, that solution would work with previous pint versions as well.
@aulemahal Can you provide a little bit more in detail you use case? I do not understand it too well.
My usecase is this : https://github.com/xarray-contrib/cf-xarray/blob/c511cc5624ef0a217e83cb0f820a7acd16b877ae/cf_xarray/units.py#L19 I myself didn't write this function, but I think the code comes from a time where this was done outside of pint, not as a custom formatter, which might explain why it looks weird.
To make things simple we begin by format the units using the short default format ("~D"). Then, using regex we split the result into its components to remove the division and exponentiation signs. Then remove the multiplication signs, replace "Δ" with "delta" and "percent" with "%".
I had not re-read the exact way this was written and I now realize this is much more complex than re-writing the units directly from the container. The only issue I foresee is that we would want the "cf" formatter to always act as a short one, as if "~cf" was written.
If you are sure that the input is unit, then recreating is not necessary.
You can also subclass the existing formatter, this would be a direct replacement to your (I haven't tried yet)
class MyFormatter(DefaultFormatter):
def format_unit(
self,
unit: PlainUnit | Iterable[tuple[str, Any]],
uspec: str = "",
sort_func: SortFunc | None = None,
**babel_kwds: Unpack[BabelKwds],
) -> str:
"""Format a unit (can be compound) into string
given a string formatting specification and locale related arguments.
"""
if "~" not in uspec:
uspec = "~" + uspec
s = super().format_unit(unit, uspec, sort_func, **babal_kwds)
# 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", "%")
# we need a better way here
ureg.formatter._formatters["cf"] = MyFormatter
and then if you look into the DefaultFormatter you can actually directly copy the parse_unit and avoid calling the super function.
By the way, I want to change the prepare_compount_unit
function to stop accepting spec
. The only spec that is used now is ~
. So I would rather have a boolean flag about using the short version.
Closing this as it makes more sense to fix the issue in cf-xarray
.
MWE:
raises: "KeyError: 'dimensionless'"
Full traceback:
To help the debug:
prints
.