Skip to content

Commit

Permalink
Merge branch 'MichaelTiemannOSC-dim_order' to augment/extend latest
Browse files Browse the repository at this point in the history
formatter changes.

Signed-off-by: Michael Tiemann <[email protected]>
  • Loading branch information
MichaelTiemannOSC committed Jan 22, 2024
2 parents 0f24b6f + 957e9ca commit f1371e5
Show file tree
Hide file tree
Showing 20 changed files with 527 additions and 324 deletions.
4 changes: 3 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Pint Changelog
0.24 (unreleased)
-----------------

- Nothing changed yet.
- Add `dim_order` property to BaseFormatter.
- Add `dim_sort` parameter to formatter.
(PR #1864, fixes Issue #1841)


0.23 (2023-12-08)
Expand Down
28 changes: 26 additions & 2 deletions docs/getting/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ a ``Quantity()`` object.
``Quantity()`` objects also work well with NumPy arrays, which you can
read about in the section on :doc:`NumPy support <numpy>`.

Some units are compound, such as [energy], which is stated in terms of
[mass] * [length]**2 / [time]**2. Earlier versions of Pint would sort unit names
alphabetically by default, leading to different orderings of units (old behavior):

```
"{:P}".format(ureg.parse_units('pound * ft**2 * second**-2')) would yield:
'foot²·pound/second²'
"{:P}".format(ureg.parse_units('kg * cm**2 * second**-2')) would yield:
'centimeter²·kilogram/second²'
```

Now by default it sorts by dimensions as proposed by ISO 80000, with [mass]
coming before [length], which also comes before [time]. The dimension order
can be changed in the registry (`dim_order` in `defaults`):

.. doctest::

>>> "{:P}".format(ureg.parse_units('pound * ft**2 * second**-2'))
'pound·foot²/second²'
>>> "{:P}".format(ureg.parse_units('kg * cm**2 * second**-2'))
'kilogram·centimeter²/second²'



Converting to different units
-----------------------------

Expand Down Expand Up @@ -180,11 +204,11 @@ but otherwise keeping your unit definitions intact.
>>> volume = 10*ureg.cc
>>> mass = density*volume
>>> print(mass)
14.0 cubic_centimeter * gram / centimeter ** 3
14.0 gram * cubic_centimeter / centimeter ** 3
>>> print(mass.to_reduced_units())
14.0 gram
>>> print(mass)
14.0 cubic_centimeter * gram / centimeter ** 3
14.0 gram * cubic_centimeter / centimeter ** 3
>>> mass.ito_reduced_units()
>>> print(mass)
14.0 gram
Expand Down
5 changes: 5 additions & 0 deletions pint/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
else:
from typing_extensions import Never # noqa

if sys.version_info >= (3, 12):
from warnings import deprecated # noqa
else:
from typing_extensions import deprecated # noqa


def missing_dependency(
package: str, display_name: Optional[str] = None
Expand Down
4 changes: 2 additions & 2 deletions pint/delegates/formatter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"""


from .base_formatter import BaseFormatter
from .base_formatter import BaseFormatter, BabelFormatter


class Formatter(BaseFormatter):
class Formatter(BabelFormatter, BaseFormatter):
# TODO: this should derive from all relevant formaters to
# reproduce the current behavior of Pint.
pass
Expand Down
252 changes: 251 additions & 1 deletion pint/delegates/formatter/base_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,44 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional, Any
import locale
from ...compat import babel_parse
import re
from ...util import UnitsContainer, iterable

from ...compat import ndarray, np
from ...formatting import (
_pretty_fmt_exponent,
extract_custom_flags,
format_unit,
ndarray_to_latex,
remove_custom_flags,
siunitx_format_unit,
split_format,
)

if TYPE_CHECKING:
from ...facets.plain import PlainQuantity, PlainUnit, MagnitudeT
from ...compat import Locale


_EXP_PATTERN = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)")


class BaseFormatter:
# This default order for sorting dimensions was described in the proposed ISO 80000 specification.
dim_order = (
"[substance]",
"[mass]",
"[current]",
"[luminosity]",
"[length]",
"[]",
"[time]",
"[temperature]",
)

def format_quantity(
self, quantity: PlainQuantity[MagnitudeT], spec: str = ""
) -> str:
Expand All @@ -25,3 +56,222 @@ def format_unit(self, unit: PlainUnit, spec: str = "") -> str:
# TODO Fill the proper functions and discuss
# how to make it that _units is not accessible directly
return " ".join(k if v == 1 else f"{k} ** {v}" for k, v in unit._units.items())


class BabelFormatter:
locale: Optional[Locale] = None
default_format: str = ""

def set_locale(self, loc: Optional[str]) -> None:
"""Change the locale used by default by `format_babel`.
Parameters
----------
loc : str or None
None` (do not translate), 'sys' (detect the system locale) or a locale id string.
"""
if isinstance(loc, str):
if loc == "sys":
loc = locale.getdefaultlocale()[0]

# We call babel parse to fail here and not in the formatting operation
babel_parse(loc)

self.locale = loc

def format_quantity(
self, quantity: PlainQuantity[MagnitudeT], spec: str = ""
) -> str:
if self.locale is not None:
return self.format_quantity_babel(quantity, spec)

registry = quantity._REGISTRY

mspec, uspec = split_format(
spec, self.default_format, registry.separate_format_defaults
)

# If Compact is selected, do it at the beginning
if "#" in spec:
# TODO: don't replace '#'
mspec = mspec.replace("#", "")
uspec = uspec.replace("#", "")
obj = quantity.to_compact()
else:
obj = quantity

del quantity

if "L" in uspec:
allf = plain_allf = r"{}\ {}"
elif "H" in uspec:
allf = plain_allf = "{} {}"
if iterable(obj.magnitude):
# Use HTML table instead of plain text template for array-likes
allf = (
"<table><tbody>"
"<tr><th>Magnitude</th>"
"<td style='text-align:left;'>{}</td></tr>"
"<tr><th>Units</th><td style='text-align:left;'>{}</td></tr>"
"</tbody></table>"
)
else:
allf = plain_allf = "{} {}"

if "Lx" in uspec:
# the LaTeX siunitx code
# TODO: add support for extracting options
opts = ""
ustr = siunitx_format_unit(obj.units._units, registry)
allf = r"\SI[%s]{{{}}}{{{}}}" % opts
else:
# Hand off to unit formatting
# TODO: only use `uspec` after completing the deprecation cycle
ustr = self.format_unit(obj.units, mspec + uspec)

# 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
mstr = obj.magnitude._repr_html_()
else:
if isinstance(obj.magnitude, ndarray):
# Use custom ndarray text formatting with monospace font
formatter = f"{{:{mspec}}}"
# Need to override for scalars, which are detected as iterable,
# and don't respond to printoptions.
if obj.magnitude.ndim == 0:
allf = plain_allf = "{} {}"
mstr = formatter.format(obj.magnitude)
else:
with np.printoptions(
formatter={"float_kind": formatter.format}
):
mstr = (
"<pre>"
+ format(obj.magnitude).replace("\n", "<br>")
+ "</pre>"
)
elif not iterable(obj.magnitude):
# Use plain text for scalars
mstr = format(obj.magnitude, mspec)
else:
# Use monospace font for other array-likes
mstr = (
"<pre>"
+ format(obj.magnitude, mspec).replace("\n", "<br>")
+ "</pre>"
)
elif isinstance(obj.magnitude, ndarray):
if "L" in uspec:
# Use ndarray LaTeX special formatting
mstr = ndarray_to_latex(obj.magnitude, mspec)
else:
# Use custom ndarray text formatting--need to handle scalars differently
# since they don't respond to printoptions
formatter = f"{{:{mspec}}}"
if obj.magnitude.ndim == 0:
mstr = formatter.format(obj.magnitude)
else:
with np.printoptions(formatter={"float_kind": formatter.format}):
mstr = format(obj.magnitude).replace("\n", "")
else:
mstr = format(obj.magnitude, mspec).replace("\n", "")

if "L" in uspec and "Lx" not in uspec:
mstr = _EXP_PATTERN.sub(r"\1\\times 10^{\2\3}", mstr)
elif "H" in uspec or "P" in uspec:
m = _EXP_PATTERN.match(mstr)
_exp_formatter = (
_pretty_fmt_exponent if "P" in uspec else lambda s: f"<sup>{s}</sup>"
)
if m:
exp = int(m.group(2) + m.group(3))
mstr = _EXP_PATTERN.sub(r"\1×10" + _exp_formatter(exp), mstr)

if allf == plain_allf and ustr.startswith("1 /"):
# Write e.g. "3 / s" instead of "3 1 / s"
ustr = ustr[2:]
return allf.format(mstr, ustr).strip()

def format_quantity_babel(
self, quantity: PlainQuantity[MagnitudeT], spec: str = "", **kwspec: Any
) -> str:
spec = spec or self.default_format

# standard cases
if "#" in spec:
spec = spec.replace("#", "")
obj = quantity.to_compact()
else:
obj = quantity

del quantity

kwspec = kwspec.copy()
if "length" in kwspec:
kwspec["babel_length"] = kwspec.pop("length")

loc = kwspec.get("locale", self.locale)
if loc is None:
raise ValueError("Provide a `locale` value to localize translation.")

kwspec["locale"] = babel_parse(loc)
kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude)
return "{} {}".format(
format(obj.magnitude, remove_custom_flags(spec)),
self.format_unit_babel(obj.units, spec, **kwspec),
).replace("\n", "")

def format_unit(self, unit: PlainUnit, spec: str = "") -> str:
registry = unit._REGISTRY

_, uspec = split_format(
spec, self.default_format, registry.separate_format_defaults
)
if "~" in uspec:
if not unit._units:
return ""
units = UnitsContainer(
{registry._get_symbol(key): value for key, value in unit._units.items()}
)
uspec = uspec.replace("~", "")
else:
units = unit._units

return format_unit(units, uspec, registry=registry)

def format_unit_babel(
self,
unit: PlainUnit,
spec: str = "",
locale: Optional[Locale] = None,
**kwspec: Any,
) -> str:
spec = spec or extract_custom_flags(self.default_format)

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

locale = self.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)

if "registry" not in kwspec:
kwspec["registry"] = unit._REGISTRY

return format_unit(units, spec, **kwspec)
3 changes: 0 additions & 3 deletions pint/facets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ class that belongs to a registry that has NumpyRegistry as one of its bases.
- plain: basic manipulation and calculation with multiplicative
dimensions, units and quantities (e.g. length, time, mass, etc).
- formatting: pretty printing and formatting modifiers.
- nonmultiplicative: manipulation and calculation with offset and
log units and quantities (e.g. temperature and decibel).
Expand Down Expand Up @@ -73,7 +71,6 @@ class that belongs to a registry that has NumpyRegistry as one of its bases.

from .context import ContextRegistry, GenericContextRegistry
from .dask import DaskRegistry, GenericDaskRegistry
from .formatting import FormattingRegistry, GenericFormattingRegistry
from .group import GroupRegistry, GenericGroupRegistry
from .measurement import MeasurementRegistry, GenericMeasurementRegistry
from .nonmultiplicative import (
Expand Down
21 changes: 0 additions & 21 deletions pint/facets/formatting/__init__.py

This file was deleted.

Loading

0 comments on commit f1371e5

Please sign in to comment.