Skip to content

Commit

Permalink
refactor: reorganized formatter and added docs
Browse files Browse the repository at this point in the history
  • Loading branch information
hgrecco committed Jan 20, 2024
1 parent b50ddc5 commit 3421f24
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 266 deletions.
4 changes: 3 additions & 1 deletion pint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from importlib.metadata import version

from .delegates.formatter._format_helpers import formatter

from .errors import ( # noqa: F401
DefinitionSyntaxError,
DimensionalityError,
Expand All @@ -25,7 +27,7 @@
UndefinedUnitError,
UnitStrippedWarning,
)
from .formatting import formatter, register_unit_format
from .formatting import register_unit_format
from .registry import ApplicationRegistry, LazyRegistry, UnitRegistry
from .util import logger, pi_theorem # noqa: F401

Expand Down
17 changes: 11 additions & 6 deletions pint/delegates/formatter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
"""
pint.delegates.formatter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Formats quantities and units.
~~~~~~~~~~~~~~~~~~~~~~~~
Easy to replace and extend string formatting.
See pint.delegates.formatter.plain.DefaultFormatter for a
description of a formatter.
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""


from .full import MultipleFormatter
from .full import FullFormatter


class Formatter(FullFormatter):
"""Default Pint Formatter"""

class Formatter(MultipleFormatter):
# TODO: this should derive from all relevant formaters to
# reproduce the current behavior of Pint.
pass


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
"""
pint.delegates.formatter._format_helpers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Convenient functions to help string formatting operations.
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""


from __future__ import annotations

from functools import partial
Expand All @@ -17,6 +28,8 @@

import locale

from pint.delegates.formatter._spec_helpers import FORMATTER, _join

from ...compat import babel_parse, ndarray
from ...util import UnitsContainer

Expand All @@ -31,6 +44,72 @@
from ...compat import Locale, Number

T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("U")


class BabelKwds(TypedDict):
"""Babel related keywords used in formatters."""

use_plural: bool
length: Literal["short", "long", "narrow"] | None
locale: Locale | str | None


def format_number(value: Any, spec: str = "") -> str:
"""Format number
This function might disapear in the future.
Right now is aiding backwards compatible migration.
"""
if isinstance(value, float):
return format(value, spec or ".16n")

elif isinstance(value, int):
return format(value, spec or "n")

elif isinstance(value, ndarray) and value.ndim == 0:
if issubclass(value.dtype.type, np_integer):
return format(value, spec or "n")
else:
return format(value, spec or ".16n")
else:
return str(value)


def builtin_format(value: Any, spec: str = "") -> str:
"""A keyword enabled replacement for builtin format
format has positional only arguments
and this cannot be partialized
and np requires a callable.
"""
return format(value, spec)


@contextmanager
def override_locale(
spec: str, locale: str | Locale | None
) -> Generator[Callable[[Any], str], Any, None]:
"""Given a spec a locale, yields a function to format a number.
IMPORTANT: When the locale is not None, this function uses setlocale
and therefore is not thread safe.
"""

if locale is None:
# If locale is None, just return the builtin format function.
yield ("{:" + spec + "}").format
else:
# If locale is not None, change it and return the backwards compatible
# format_number.
prev_locale_string = getlocale(LC_NUMERIC)
if isinstance(locale, str):
setlocale(LC_NUMERIC, locale)
else:
setlocale(LC_NUMERIC, str(locale))
yield partial(format_number, spec=spec)
setlocale(LC_NUMERIC, prev_locale_string)


def format_unit_no_magnitude(
Expand Down Expand Up @@ -115,23 +194,25 @@ def format_unit_no_magnitude(
return f"{fallback_name or measurement_unit}" # pragma: no cover


def _unit_mapper(
units: Iterable[tuple[str, T]],
shortener: Callable[
def map_keys(
func: Callable[
[
str,
T,
],
str,
U,
],
) -> Iterable[tuple[str, T]]:
return map(lambda el: (shortener(el[0]), el[1]), units)
items: Iterable[tuple[T, V]],
) -> Iterable[tuple[U, V]]:
"""Map dict keys given an items view."""
return map(lambda el: (func(el[0]), el[1]), items)


def short_form(
units: Iterable[tuple[str, T]],
registry: UnitRegistry,
) -> Iterable[tuple[str, T]]:
return _unit_mapper(units, registry.get_symbol)
"""Replace each unit by its short form."""
return map_keys(registry.get_symbol, units)


def localized_form(
Expand All @@ -140,20 +221,15 @@ def localized_form(
length: Literal["short", "long", "narrow"],
locale: Locale | str,
) -> Iterable[tuple[str, T]]:
"""Replace each unit by its localized version."""
mapper = partial(
format_unit_no_magnitude,
use_plural=use_plural,
length=length,
locale=babel_parse(locale),
)

return _unit_mapper(units, mapper)


class BabelKwds(TypedDict):
use_plural: bool
length: Literal["short", "long", "narrow"] | None
locale: Locale | str | None
return map_keys(mapper, units)


def format_compound_unit(
Expand All @@ -163,6 +239,10 @@ def format_compound_unit(
length: Literal["short", "long", "narrow"] | None = None,
locale: Locale | str | None = None,
) -> Iterable[tuple[str, Number]]:
"""Format compound unit into unit container given
an spec and locale.
"""

# TODO: provisional? Should we allow unbounded units?
# Should we allow UnitsContainer?
registry = getattr(unit, "_REGISTRY", None)
Expand All @@ -187,41 +267,88 @@ def format_compound_unit(
return out


def format_number(value: Any, spec: str = "") -> str:
if isinstance(value, float):
return format(value, spec or ".16n")
def formatter(
items: Iterable[tuple[str, Number]],
as_ratio: bool = True,
single_denominator: bool = False,
product_fmt: str = " * ",
division_fmt: str = " / ",
power_fmt: str = "{} ** {}",
parentheses_fmt: str = "({0})",
exp_call: FORMATTER = "{:n}".format,
sort: bool = True,
) -> str:
"""Format a list of (name, exponent) pairs.
Parameters
----------
items : list
a list of (name, exponent) pairs.
as_ratio : bool, optional
True to display as ratio, False as negative powers. (Default value = True)
single_denominator : bool, optional
all with terms with negative exponents are
collected together. (Default value = False)
product_fmt : str
the format used for multiplication. (Default value = " * ")
division_fmt : str
the format used for division. (Default value = " / ")
power_fmt : str
the format used for exponentiation. (Default value = "{} ** {}")
parentheses_fmt : str
the format used for parenthesis. (Default value = "({0})")
exp_call : callable
(Default value = lambda x: f"{x:n}")
sort : bool, optional
True to sort the formatted units alphabetically (Default value = True)
Returns
-------
str
the formula as a string.
elif isinstance(value, int):
return format(value, spec or "n")
"""

elif isinstance(value, ndarray) and value.ndim == 0:
if issubclass(value.dtype.type, np_integer):
return format(value, spec or "n")
else:
return format(value, spec or ".16n")
if sort:
items = sorted(items)
else:
return str(value)
items = tuple(items)

if not items:
return ""

# TODO: ugly, ugly
# format has positional only arguments
# and this cannot be partialized
# and np requires a callable. We could create a lambda
def builtin_format(value: Any, spec: str = "") -> str:
return format(value, spec)
if as_ratio:
fun = lambda x: exp_call(abs(x))
else:
fun = exp_call

pos_terms, neg_terms = [], []

@contextmanager
def override_locale(
spec: str, locale: str | Locale | None
) -> Generator[Callable[[Any], str], Any, None]:
if locale is None:
yield ("{:" + spec + "}").format
else:
prev_locale_string = getlocale(LC_NUMERIC)
if isinstance(locale, str):
setlocale(LC_NUMERIC, locale)
for key, value in items:
if value == 1:
pos_terms.append(key)
elif value > 0:
pos_terms.append(power_fmt.format(key, fun(value)))
elif value == -1 and as_ratio:
neg_terms.append(key)
else:
setlocale(LC_NUMERIC, str(locale))
yield partial(format_number, spec=spec)
setlocale(LC_NUMERIC, prev_locale_string)
neg_terms.append(power_fmt.format(key, fun(value)))

if not as_ratio:
# Show as Product: positive * negative terms ** -1
return _join(product_fmt, pos_terms + neg_terms)

# Show as Ratio: positive terms / negative terms
pos_ret = _join(product_fmt, pos_terms) or "1"

if not neg_terms:
return pos_ret

if single_denominator:
neg_ret = _join(product_fmt, neg_terms)
if len(neg_terms) > 1:
neg_ret = parentheses_fmt.format(neg_ret)
else:
neg_ret = _join(division_fmt, neg_terms)

return _join(division_fmt, [pos_ret, neg_ret])
Loading

0 comments on commit 3421f24

Please sign in to comment.