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

custom formatters: pass unit modifiers to the formatter #1448

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Pint Changelog
- Fix setting options of the application registry (Issue #1403).
- Fix Quantity & Unit `is_compatible_with` with registry active contexts (Issue #1424).
- Allow Quantity to parse 'NaN' and 'inf(inity)', case insensitive
- Pass unit modifiers to custom formatters (Issue #1448)
- Fix casting error when using to_reduced_units with array of int.
(Issue #1184)
- Use default numpy `np.printoptions` available since numpy 1.15.
Expand Down
30 changes: 2 additions & 28 deletions pint/facets/formatting/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
split_format,
)
from ...util import iterable
from ..plain import UnitsContainer


class FormattingQuantity:
Expand Down Expand Up @@ -186,42 +185,17 @@ def __format__(self, spec) -> str:
_, uspec = split_format(
spec, self.default_format, self._REGISTRY.separate_format_defaults
)
if "~" in uspec:
if not self._units:
return ""
units = UnitsContainer(
dict(
(self._REGISTRY._get_symbol(key), value)
for key, value in self._units.items()
)
)
uspec = uspec.replace("~", "")
else:
units = self._units

return format_unit(units, uspec, registry=self._REGISTRY)
return format_unit(self, uspec, registry=self._REGISTRY)

def format_babel(self, spec="", locale=None, **kwspec: Any) -> str:
spec = spec or extract_custom_flags(self.default_format)

if "~" in spec:
if self.dimensionless:
return ""
units = UnitsContainer(
dict(
(self._REGISTRY._get_symbol(key), value)
for key, value in self._units.items()
)
)
spec = spec.replace("~", "")
else:
units = self._units

locale = self._REGISTRY.fmt_locale if locale is None else locale

if locale is None:
raise ValueError("Provide a `locale` value to localize translation.")
else:
kwspec["locale"] = babel_parse(locale)

return units.format_babel(spec, registry=self._REGISTRY, **kwspec)
return self._units.format_babel(spec, registry=self._REGISTRY, **kwspec)
98 changes: 70 additions & 28 deletions pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,44 @@ def wrapper(func):
return wrapper


def apply_unit_modifiers(unit, modifiers, registry):
"""apply modifiers to a unit

Parameters
----------
unit : Unit or UnitsContainer
The input unit.
modifiers : str
The modifiers to apply.
registry : UnitRegistry
The unit registry associated with `unit`.

Returns
-------
UnitsContainer
A dict-like container with the preprocessed unit components.
"""
from .util import UnitsContainer

raw = unit if isinstance(unit, UnitsContainer) else unit._units
if not raw:
return UnitsContainer({})

if "~" in modifiers:
applied = UnitsContainer(
{registry._get_symbol(key): value for key, value in raw.items()}
)
else:
applied = raw

return applied


@register_unit_format("P")
def format_pretty(unit, registry, **options):
def format_pretty(unit, registry, modifiers, **options):
modified = apply_unit_modifiers(unit, modifiers, registry)
return formatter(
unit.items(),
modified.items(),
as_ratio=True,
single_denominator=False,
product_fmt="·",
Expand All @@ -179,9 +213,10 @@ def format_pretty(unit, registry, **options):


@register_unit_format("L")
def format_latex(unit, registry, **options):
def format_latex(unit, registry, modifiers, **options):
modified = apply_unit_modifiers(unit, modifiers, registry)
preprocessed = {
r"\mathrm{{{}}}".format(u.replace("_", r"\_")): p for u, p in unit.items()
r"\mathrm{{{}}}".format(u.replace("_", r"\_")): p for u, p in modified.items()
}
formatted = formatter(
preprocessed.items(),
Expand All @@ -197,7 +232,7 @@ def format_latex(unit, registry, **options):


@register_unit_format("Lx")
def format_latex_siunitx(unit, registry, **options):
def format_latex_siunitx(unit, registry, modifiers, **options):
if registry is None:
raise ValueError(
"Can't format as siunitx without a registry."
Expand All @@ -206,14 +241,16 @@ def format_latex_siunitx(unit, registry, **options):
" and might indicate a bug in `pint`."
)

formatted = siunitx_format_unit(unit, registry)
modified = apply_unit_modifiers(unit, modifiers, registry)
formatted = siunitx_format_unit(modified, registry)
return rf"\si[]{{{formatted}}}"


@register_unit_format("H")
def format_html(unit, registry, **options):
def format_html(unit, registry, modifiers, **options):
modified = apply_unit_modifiers(unit, modifiers, registry)
return formatter(
unit.items(),
modified.items(),
as_ratio=True,
single_denominator=True,
product_fmt=r" ",
Expand All @@ -225,9 +262,10 @@ def format_html(unit, registry, **options):


@register_unit_format("D")
def format_default(unit, registry, **options):
def format_default(unit, registry, modifiers, **options):
modified = apply_unit_modifiers(unit, modifiers, registry)
return formatter(
unit.items(),
modified.items(),
as_ratio=True,
single_denominator=False,
product_fmt=" * ",
Expand All @@ -239,9 +277,10 @@ def format_default(unit, registry, **options):


@register_unit_format("C")
def format_compact(unit, registry, **options):
def format_compact(unit, registry, modifiers, **options):
modified = apply_unit_modifiers(unit, modifiers, registry)
return formatter(
unit.items(),
modified.items(),
as_ratio=True,
single_denominator=False,
product_fmt="*", # TODO: Should this just be ''?
Expand Down Expand Up @@ -374,14 +413,15 @@ def formatter(
# http://docs.python.org/2/library/string.html#format-specification-mini-language
# We also add uS for uncertainties.
_BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS")
_UNIT_MODIFIERS = frozenset("~")


def _parse_spec(spec):
result = ""
for ch in reversed(spec):
if ch == "~" or ch in _BASIC_TYPES:
continue
elif ch in list(_FORMATTERS.keys()) + ["~"]:
elif ch in list(_FORMATTERS.keys()) + list(_UNIT_MODIFIERS):
if result:
raise ValueError("expected ':' after format specifier")
else:
Expand All @@ -403,14 +443,15 @@ def format_unit(unit, spec, registry=None, **options):
else:
return "dimensionless"

if not spec:
spec = "D"
uspec, modifiers = split_unit_format(spec)
if not uspec:
uspec = "D"

fmt = _FORMATTERS.get(spec)
fmt = _FORMATTERS.get(uspec)
if fmt is None:
raise ValueError(f"Unknown conversion specified: {spec}")

return fmt(unit, registry=registry, **options)
return fmt(unit, registry=registry, modifiers=modifiers, **options)


def siunitx_format_unit(units, registry):
Expand Down Expand Up @@ -455,26 +496,27 @@ def _tothe(power):
return "".join(lpos) + "".join(lneg)


def extract_custom_flags(spec):
import re
def split_unit_format(uspec):
modifiers_re = re.compile(rf"[{''.join(_UNIT_MODIFIERS)}]")
modifiers = "".join(modifiers_re.findall(uspec))
uspec = modifiers_re.sub("", uspec)

if not spec:
return ""
return uspec, modifiers

# sort by length, with longer items first
known_flags = sorted(_FORMATTERS.keys(), key=len, reverse=True)

flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")")
def extract_custom_flags(spec):
flags = sorted(_FORMATTERS, key=len, reverse=True) + list(_UNIT_MODIFIERS)
flag_re = re.compile("|".join(flags))

custom_flags = flag_re.findall(spec)

return "".join(custom_flags)


def remove_custom_flags(spec):
for flag in sorted(_FORMATTERS.keys(), key=len, reverse=True) + ["~"]:
if flag:
spec = spec.replace(flag, "")
return spec
flags = sorted(_FORMATTERS, key=len, reverse=True) + list(_UNIT_MODIFIERS)
flag_re = re.compile("|".join(flags))
return flag_re.sub("", spec)


def split_format(spec, default, separate_format_defaults=True):
Expand Down
15 changes: 15 additions & 0 deletions pint/testsuite/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ def format_new(unit, **options):

assert "{:new}".format(ureg.m) == "new format"

def test_unit_formatting_custom_modifiers(self, monkeypatch):
from pint import formatting, register_unit_format

monkeypatch.setattr(formatting, "_FORMATTERS", formatting._FORMATTERS.copy())

@register_unit_format("new")
def format_new(unit, *, modifiers, **options):
return f"new format for {unit:~P} with '{modifiers}'"

ureg = UnitRegistry()
u = ureg.m

assert f"{u:new}" == "new format for m with ''"
assert f"{u:~new}" == "new format for m with '~'"

def test_ipython(self):
alltext = []

Expand Down