diff --git a/CHANGES b/CHANGES index 142c95a6b..7a2f80bf5 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Pint Changelog 0.19 (unreleased) ----------------- +- Deprecate the old format defaulting behavior and prepare for the new one (Issue #1407) - Fix a bug for offset units of higher dimension, e.g. gauge pressure. (Issue #1066, thanks dalito) - Fix type hints of function wrapper (Issue #1431) diff --git a/docs/formatting.rst b/docs/formatting.rst index 74fb8bdd6..5b4c91f06 100644 --- a/docs/formatting.rst +++ b/docs/formatting.rst @@ -20,16 +20,45 @@ specifications `. The basic format is: where each part is optional and the order of these is arbitrary. -In case any part (except the modifier) is omitted, the corresponding value in -:py:attr:`Quantity.default_format` or :py:attr:`Unit.default_format` is filled in. If -that is not set (it evaluates to ``False``), :py:attr:`UnitRegistry.default_format` is -used. If both are not set, the global default of ``"D"`` and the magnitude's default +In case the format is omitted, the corresponding value in the object's +``.default_format`` attribute (:py:attr:`Quantity.default_format` or +:py:attr:`Unit.default_format`) is filled in. For example: + +.. ipython:: + + In [1]: ureg = pint.UnitRegistry() + ...: ureg.default_format = "~P" + + In [2]: u = ureg.Unit("m ** 2 / s ** 2") + ...: f"{u}" + + In [3]: u.default_format = "~C" + ...: f"{u}" + + In [4]: u.default_format, ureg.default_format + + In [5]: q = ureg.Quantity(1.25, "m ** 2 / s ** 2") + ...: f"{q}" + + In [6]: q.default_format = ".3fP" + ...: f"{q}" + + In [7]: q.default_format, ureg.default_format + +.. note:: + + In the future, the magnitude and unit format spec will be evaluated + independently, such that with a global default of + ``ureg.default_format = ".3f"`` and ``f"{q:P}`` the format that + will be used is ``".3fP"``. + +If both are not set, the global default of ``"D"`` and the magnitude's default format are used instead. .. note:: Modifiers may be used without specifying any format: ``"~"`` is a valid format - specification. + specification and is equal to ``"~D"``. Unit Format Specifications diff --git a/pint/formatting.py b/pint/formatting.py index a04205dd9..5a458db91 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -455,14 +455,20 @@ def _tothe(power): def extract_custom_flags(spec): import re - flag_re = re.compile("(" + "|".join(list(_FORMATTERS.keys()) + ["~"]) + ")") + if not spec: + return "" + + # sort by length, with longer items first + known_flags = sorted(_FORMATTERS.keys(), key=len, reverse=True) + + flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")") custom_flags = flag_re.findall(spec) return "".join(custom_flags) def remove_custom_flags(spec): - for flag in list(_FORMATTERS.keys()) + ["~"]: + for flag in sorted(_FORMATTERS.keys(), key=len, reverse=True) + ["~"]: if flag: spec = spec.replace(flag, "") return spec diff --git a/pint/quantity.py b/pint/quantity.py index 2b89df3c8..e74287405 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -345,18 +345,47 @@ def __format__(self, spec: str) -> str: if self._REGISTRY.fmt_locale is not None: return self.format_babel(spec) - spec = spec or self.default_format + mspec = remove_custom_flags(spec) + uspec = extract_custom_flags(spec) + + default_mspec = remove_custom_flags(self.default_format) + default_uspec = extract_custom_flags(self.default_format) + if spec: + if not uspec and default_uspec: + warnings.warn( + ( + "The given format spec does not contain a unit formatter." + " Falling back to the builtin defaults, but in the future" + " the unit formatter specified in the `default_format`" + " attribute will be used instead." + ), + DeprecationWarning, + ) + if not mspec and default_mspec: + warnings.warn( + ( + "The given format spec does not contain a magnitude formatter." + " Falling back to the builtin defaults, but in the future" + " the magnitude formatter specified in the `default_format`" + " attribute will be used instead." + ), + DeprecationWarning, + ) + else: + mspec, uspec = default_mspec, default_uspec # If Compact is selected, do it at the beginning if "#" in spec: - spec = spec.replace("#", "") + # TODO: don't replace '#' + mspec = mspec.replace("#", "") + uspec = uspec.replace("#", "") obj = self.to_compact() else: obj = self - if "L" in spec: + if "L" in uspec: allf = plain_allf = r"{}\ {}" - elif "H" in spec: + elif "H" in uspec: allf = plain_allf = "{} {}" if iterable(obj.magnitude): # Use HTML table instead of plain text template for array-likes @@ -370,20 +399,19 @@ def __format__(self, spec: str) -> str: else: allf = plain_allf = "{} {}" - if "Lx" in spec: + if "Lx" in uspec: # the LaTeX siunitx code - spec = spec.replace("Lx", "") # TODO: add support for extracting options opts = "" ustr = siunitx_format_unit(obj.units._units, obj._REGISTRY) allf = r"\SI[%s]{{{}}}{{{}}}" % opts else: # Hand off to unit formatting - uspec = extract_custom_flags(spec) - ustr = format(obj.units, uspec) + # TODO: only use `uspec` after completing the deprecation cycle + ustr = format(obj.units, mspec + uspec) - mspec = remove_custom_flags(spec) - if "H" in spec: + # mspec = remove_custom_flags(spec) + if "H" in uspec: # HTML formatting if hasattr(obj.magnitude, "_repr_html_"): # If magnitude has an HTML repr, nest it within Pint's @@ -417,7 +445,7 @@ def __format__(self, spec: str) -> str: + "" ) elif isinstance(self.magnitude, ndarray): - if "L" in spec: + if "L" in uspec: # Use ndarray LaTeX special formatting mstr = ndarray_to_latex(obj.magnitude, mspec) else: @@ -432,12 +460,12 @@ def __format__(self, spec: str) -> str: else: mstr = format(obj.magnitude, mspec).replace("\n", "") - if "L" in spec: + if "L" in uspec: mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr) - elif "H" in spec or "P" in spec: + elif "H" in uspec or "P" in uspec: m = self._exp_pattern.match(mstr) _exp_formatter = ( - _pretty_fmt_exponent if "P" in spec else lambda s: f"{s}" + _pretty_fmt_exponent if "P" in uspec else lambda s: f"{s}" ) if m: exp = int(m.group(2) + m.group(3)) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 503007462..c29fd19f5 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -262,6 +262,24 @@ def test_default_formatting(self, subtests): ureg.default_format = spec assert f"{x}" == result + def test_formatting_override_default_units(self): + ureg = UnitRegistry() + ureg.default_format = "~" + x = ureg.Quantity(4, "m ** 2") + + assert f"{x:dP}" == "4 meterĀ²" + with pytest.warns(DeprecationWarning): + assert f"{x:d}" == "4 meter ** 2" + + def test_formatting_override_default_magnitude(self): + ureg = UnitRegistry() + ureg.default_format = ".2f" + x = ureg.Quantity(4, "m ** 2") + + assert f"{x:dP}" == "4 meterĀ²" + with pytest.warns(DeprecationWarning): + assert f"{x:D}" == "4 meter ** 2" + def test_exponent_formatting(self): ureg = UnitRegistry() x = ureg.Quantity(1e20, "meter") diff --git a/pint/unit.py b/pint/unit.py index 8221d1953..18a22dec5 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -80,7 +80,7 @@ def __repr__(self) -> str: return "".format(self._units) def __format__(self, spec) -> str: - spec = spec or extract_custom_flags(self.default_format) + spec = extract_custom_flags(spec or self.default_format) if "~" in spec: if not self._units: return ""