From 9320a32227bb0a8e5ab45bf81a25ea311d8cbdba Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 10:48:35 -0400
Subject: [PATCH 01/13] Implement sorting units by dimension order
Once upon a time, ISO 80000 attempted to codify preferences for ordering units when describing quantities, for example `kW h` (not `h Kw`). While they have withdrawn the standard, there are many publications that state a preference for ordering units when describing quantities.
Pint allows users to choose to sort units alphabetically or not (thus `kW * h` becomes `h * kW`, whereas `kW * s` retmains `kW * s` becase `kW` sorts as less than `s`).
This PR adds a `sort_dims` parameter to `pint.formatting.formatter` which can be used in conjunction with alphabetical sorting. `sort_dims` imposes a "most significant dimension" order on the units, which are then sorted alphebetically (or not) within each dimension type.
This addresses #1841. It is intended as a prototype to evaluate as pint's formatting roadmap evolves. In particular, it needs a way to make `sort_dims` more accessible to the user.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
pint/formatting.py | 61 +++++++++++++++++++++++++++++++++++
pint/testsuite/test_issues.py | 17 ++++++++++
2 files changed, 78 insertions(+)
diff --git a/pint/formatting.py b/pint/formatting.py
index b00b771c7..d259ab615 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -197,6 +197,7 @@ def format_pretty(unit: UnitsContainer, registry: UnitRegistry, **options) -> st
power_fmt="{}{}",
parentheses_fmt="({})",
exp_call=_pretty_fmt_exponent,
+ registry=registry,
**options,
)
@@ -228,6 +229,7 @@ def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
+ registry=registry,
**options,
)
return formatted.replace("[", "{").replace("]", "}")
@@ -259,6 +261,7 @@ def format_html(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
division_fmt=r"{}/{}",
power_fmt=r"{}{}",
parentheses_fmt=r"({})",
+ registry=registry,
**options,
)
@@ -273,6 +276,7 @@ def format_default(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
division_fmt=" / ",
power_fmt="{} ** {}",
parentheses_fmt=r"({})",
+ registry=registry,
**options,
)
@@ -287,10 +291,53 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
division_fmt="/",
power_fmt="{}**{}",
parentheses_fmt=r"({})",
+ registry=registry,
**options,
)
+dim_order = [ '[substance]', '[mass]', '[current]', '[luminosity]', '[length]', '[time]', '[temperature]' ]
+def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
+ """Sort a list of units by dimensional order.
+
+ Parameters
+ ----------
+ units : list
+ a list of unit names (without values).
+ registry : UnitRegistry
+ the registry to use for looking up the dimensions of each unit.
+
+ Returns
+ -------
+ list
+ the list of units sorted by most significant dimension first.
+
+ Raises
+ ------
+ KeyError
+ If unit cannot be found in the registry.
+ """
+ ret_dict = dict()
+ many = len(units) > 1
+ for name in units:
+ cname = registry.get_name(name)
+ if not cname:
+ continue
+ dim_types = iter(dim_order)
+ while True:
+ try:
+ dim = next(dim_types)
+ if dim in registry.get_dimensionality(cname):
+ if dim not in ret_dict:
+ ret_dict[dim] = list()
+ ret_dict[dim].append(cname)
+ break
+ except StopIteration:
+ raise KeyError(f"Unit {cname} has no recognized dimensions")
+
+ ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], [])
+ return ret
+
def formatter(
items: Iterable[tuple[str, Number]],
as_ratio: bool = True,
@@ -304,6 +351,8 @@ def formatter(
babel_length: str = "long",
babel_plural_form: str = "one",
sort: bool = True,
+ sort_dims: bool = False,
+ registry: Optional[UnitRegistry] = None,
) -> str:
"""Format a list of (name, exponent) pairs.
@@ -334,6 +383,12 @@ def formatter(
(Default value = lambda x: f"{x:n}")
sort : bool, optional
True to sort the formatted units alphabetically (Default value = True)
+ sort_dims : bool, optional
+ True to sort the units dimentionally (Default value = False).
+ When dimensions have multiple units, sort by "most significant dimension" the unit contains
+ When both `sort` and `sort_dims` are True, sort alphabetically within sorted dimensions
+ registry : UnitRegistry, optional
+ The registry to use if `sort_dims` is True
Returns
-------
@@ -393,6 +448,12 @@ def formatter(
else:
neg_terms.append(power_fmt.format(key, fun(value)))
+ if sort_dims:
+ if len(pos_terms)>1:
+ pos_terms = dim_sort(pos_terms, registry)
+ if len(neg_terms)>1:
+ neg_terms = dim_sort(neg_terms, registry)
+
if not as_ratio:
# Show as Product: positive * negative terms ** -1
return _join(product_fmt, pos_terms + neg_terms)
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index c98ac61bf..c4bdd2b5d 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -1150,3 +1150,20 @@ def test_issues_1505():
assert isinstance(
ur.Quantity("m/s").magnitude, decimal.Decimal
) # unexpected fail (magnitude should be a decimal)
+
+
+def test_issues_1841():
+ import pint
+
+ # sets compact display mode
+ ur = UnitRegistry()
+ ur.default_format = '~P'
+
+ # creates quantity
+ q = ur.Quantity("1 kW * 1 h")
+
+ assert pint.formatting.format_unit(q.u._units, spec='', registry=ur, sort_dims=True) == 'kilowatt * hour'
+ assert pint.formatting.format_unit(q.u._units, spec='', registry=ur, sort_dims=False) == 'hour * kilowatt'
+
+ # this prints "1 h·kW", not "1 kW·h" unless sort_dims is True
+ # print(q)
From cdf7e6541149e9e1dd3f9626716675bc5265abda Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 10:52:31 -0400
Subject: [PATCH 02/13] pre-commit fixes
Forgot to run pre-commit for initial commit.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
pint/formatting.py | 19 +++++++++++++++----
pint/testsuite/test_issues.py | 12 +++++++++---
2 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/pint/formatting.py b/pint/formatting.py
index d259ab615..90613d997 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -296,7 +296,17 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
)
-dim_order = [ '[substance]', '[mass]', '[current]', '[luminosity]', '[length]', '[time]', '[temperature]' ]
+dim_order = [
+ "[substance]",
+ "[mass]",
+ "[current]",
+ "[luminosity]",
+ "[length]",
+ "[time]",
+ "[temperature]",
+]
+
+
def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
"""Sort a list of units by dimensional order.
@@ -318,7 +328,7 @@ def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
If unit cannot be found in the registry.
"""
ret_dict = dict()
- many = len(units) > 1
+ len(units) > 1
for name in units:
cname = registry.get_name(name)
if not cname:
@@ -338,6 +348,7 @@ def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], [])
return ret
+
def formatter(
items: Iterable[tuple[str, Number]],
as_ratio: bool = True,
@@ -449,9 +460,9 @@ def formatter(
neg_terms.append(power_fmt.format(key, fun(value)))
if sort_dims:
- if len(pos_terms)>1:
+ if len(pos_terms) > 1:
pos_terms = dim_sort(pos_terms, registry)
- if len(neg_terms)>1:
+ if len(neg_terms) > 1:
neg_terms = dim_sort(neg_terms, registry)
if not as_ratio:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index c4bdd2b5d..3ee0f11df 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -1157,13 +1157,19 @@ def test_issues_1841():
# sets compact display mode
ur = UnitRegistry()
- ur.default_format = '~P'
+ ur.default_format = "~P"
# creates quantity
q = ur.Quantity("1 kW * 1 h")
- assert pint.formatting.format_unit(q.u._units, spec='', registry=ur, sort_dims=True) == 'kilowatt * hour'
- assert pint.formatting.format_unit(q.u._units, spec='', registry=ur, sort_dims=False) == 'hour * kilowatt'
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True)
+ == "kilowatt * hour"
+ )
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False)
+ == "hour * kilowatt"
+ )
# this prints "1 h·kW", not "1 kW·h" unless sort_dims is True
# print(q)
From 1d7327a850aa8fdce71edfe936cadf7ad4f65296 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 17:34:28 -0400
Subject: [PATCH 03/13] Make sort_dims default and sorting user-defineable
Per suggestions from @andrewgsavage make `sort_dims` default. Also allow users to specify dimensional sorting in registry definition.
Also fix a number of sub-tests not previously tested. Tests all pass without any drama.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
CHANGES | 2 +
pint/default_en.txt | 1 +
pint/facets/plain/definitions.py | 6 ++-
pint/formatting.py | 79 +++++++++++++++++++-------------
pint/testsuite/test_issues.py | 54 ++++++++++++++++++++++
5 files changed, 109 insertions(+), 33 deletions(-)
diff --git a/CHANGES b/CHANGES
index 1cde05402..0b8d625d3 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,6 +4,8 @@ Pint Changelog
0.23 (unreleased)
-----------------
+- Add `dim_sort` parameter to formatter.
+ (PR #1864, fixes Issue #1841)
- Fixed Transformation type protocol.
(PR #1805, PR #1832)
- Documented to_preferred and created added an autoautoconvert_to_preferred registry option.
diff --git a/pint/default_en.txt b/pint/default_en.txt
index 5fc7f8265..391eeb37a 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -56,6 +56,7 @@
@defaults
group = international
system = mks
+ dim_order = [ "[substance]", "[mass]", "[current]", "[luminosity]", "[length]", "[]", "[time]", "[temperature]", ]
@end
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index 44bf29858..05cb055a0 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -11,9 +11,10 @@
import itertools
import numbers
import typing as ty
+import ast
from dataclasses import dataclass
from functools import cached_property
-from typing import Any, Optional
+from typing import Any, List, Optional
from ..._typing import Magnitude
from ... import errors
@@ -60,12 +61,15 @@ class DefaultsDefinition:
group: ty.Optional[str]
system: ty.Optional[str]
+ dim_order: ty.Optional[List[str]]
def items(self):
if self.group is not None:
yield "group", self.group
if self.system is not None:
yield "system", self.system
+ if self.dim_order is not None:
+ yield "dim_order", ast.literal_eval(self.dim_order)
@dataclass(frozen=True)
diff --git a/pint/formatting.py b/pint/formatting.py
index 90613d997..b158da623 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -13,7 +13,8 @@
import functools
import re
import warnings
-from typing import Callable, Any, TYPE_CHECKING, TypeVar, Optional, Union
+from typing import Callable, Any, TYPE_CHECKING, TypeVar, List, Optional, Tuple, Union
+from collections import OrderedDict
from collections.abc import Iterable
from numbers import Number
@@ -220,7 +221,18 @@ def latex_escape(string: str) -> str:
@register_unit_format("L")
def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
- preprocessed = {rf"\mathrm{{{latex_escape(u)}}}": p for u, p in unit.items()}
+ # Lift the sorting by dimensions b/c the preprocessed units are unrecognizeable
+ sorted_units = dim_sort(unit.items(), registry)
+ preprocessed_list = [
+ (
+ rf"\mathrm{{{latex_escape(u)}}}",
+ p,
+ )
+ for u, p in sorted_units
+ ]
+ preprocessed = OrderedDict()
+ for k, v in preprocessed_list:
+ preprocessed[k] = v
formatted = formatter(
preprocessed.items(),
as_ratio=True,
@@ -229,6 +241,8 @@ def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
+ sort=False,
+ sort_dims=False,
registry=registry,
**options,
)
@@ -296,24 +310,13 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
)
-dim_order = [
- "[substance]",
- "[mass]",
- "[current]",
- "[luminosity]",
- "[length]",
- "[time]",
- "[temperature]",
-]
-
-
-def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
+def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
"""Sort a list of units by dimensional order.
Parameters
----------
- units : list
- a list of unit names (without values).
+ items : tuple
+ a list of tuples containing (unit names, exponent values).
registry : UnitRegistry
the registry to use for looking up the dimensions of each unit.
@@ -327,30 +330,46 @@ def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
KeyError
If unit cannot be found in the registry.
"""
+ if registry is None or len(items) <= 1:
+ return items
+ # if len(items) == 2 and items[0][1] * items[1][1] < 0:
+ # return items
ret_dict = dict()
- len(units) > 1
- for name in units:
+ for name, value in items:
cname = registry.get_name(name)
if not cname:
continue
- dim_types = iter(dim_order)
+ cname_dims = registry.get_dimensionality(cname)
+ if len(cname_dims) == 0:
+ cname_dims = {"[]": None}
+ dim_types = iter(registry._defaults["dim_order"])
while True:
try:
dim = next(dim_types)
- if dim in registry.get_dimensionality(cname):
+ if dim in cname_dims:
if dim not in ret_dict:
ret_dict[dim] = list()
- ret_dict[dim].append(cname)
+ ret_dict[dim].append(
+ (
+ name,
+ value,
+ )
+ )
break
except StopIteration:
- raise KeyError(f"Unit {cname} has no recognized dimensions")
+ raise KeyError(
+ f"Unit {name} (aka {cname}) has no recognized dimensions"
+ )
- ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], [])
+ ret = sum(
+ [ret_dict[dim] for dim in registry._defaults["dim_order"] if dim in ret_dict],
+ [],
+ )
return ret
def formatter(
- items: Iterable[tuple[str, Number]],
+ items: Iterable[Tuple[str, Number]],
as_ratio: bool = True,
single_denominator: bool = False,
product_fmt: str = " * ",
@@ -362,7 +381,7 @@ def formatter(
babel_length: str = "long",
babel_plural_form: str = "one",
sort: bool = True,
- sort_dims: bool = False,
+ sort_dims: bool = True,
registry: Optional[UnitRegistry] = None,
) -> str:
"""Format a list of (name, exponent) pairs.
@@ -420,6 +439,8 @@ def formatter(
if sort:
items = sorted(items)
+ if sort_dims:
+ items = dim_sort(items, registry)
for key, value in items:
if locale and babel_length and babel_plural_form and key in _babel_units:
_key = _babel_units[key]
@@ -459,12 +480,6 @@ def formatter(
else:
neg_terms.append(power_fmt.format(key, fun(value)))
- if sort_dims:
- if len(pos_terms) > 1:
- pos_terms = dim_sort(pos_terms, registry)
- if len(neg_terms) > 1:
- neg_terms = dim_sort(neg_terms, registry)
-
if not as_ratio:
# Show as Product: positive * negative terms ** -1
return _join(product_fmt, pos_terms + neg_terms)
@@ -640,7 +655,7 @@ def vector_to_latex(vec: Iterable[Any], fmtfun: FORMATTER = ".2f".format) -> str
def matrix_to_latex(matrix: ItMatrix, fmtfun: FORMATTER = ".2f".format) -> str:
- ret: list[str] = []
+ ret: List[str] = []
for row in matrix:
ret += [" & ".join(fmtfun(f) for f in row)]
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 3ee0f11df..7702ea2eb 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -1173,3 +1173,57 @@ def test_issues_1841():
# this prints "1 h·kW", not "1 kW·h" unless sort_dims is True
# print(q)
+
+ q = ur.Quantity("1 kV * A")
+
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True)
+ == "kilovolt * ampere"
+ )
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False)
+ == "ampere * kilovolt"
+ )
+
+ # this prints "1 A·kV", not "1 kV·A" unless sort_dims is True
+ # print(q)
+
+ q = ur.Quantity("1 N * m")
+
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True)
+ == "newton * meter"
+ )
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False)
+ == "meter * newton"
+ )
+
+ # this prints "1 m·N", not "1 N·m" unless sort_dims is True
+ # print(q)
+
+
+@pytest.mark.xfail
+def test_issues_1841_xfail():
+ import pint
+
+ # sets compact display mode
+ ur = UnitRegistry()
+ ur.default_format = "~P"
+
+ q = ur.Quantity("2*pi radian * hour")
+
+ # Note that `radian` (and `bit` and `count`) are treated as dimensionless.
+ # And note that dimensionless quantities are stripped by this process,
+ # leading to errorneous output. Suggestions?
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True)
+ == "radian * hour"
+ )
+ assert (
+ pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False)
+ == "hour * radian"
+ )
+
+ # this prints "2*pi hour * radian", not "2*pi radian * hour" unless sort_dims is True
+ # print(q)
From eae68562c4663577b80fbfe50ee63e00ace7b721 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 18:35:32 -0400
Subject: [PATCH 04/13] Documentation and code cleanups
Incorporate more code review feedback and fix some doc issues.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
docs/getting/tutorial.rst | 24 ++++++++++++++++++++++--
pint/default_en.txt | 1 +
pint/formatting.py | 33 ++++++++++++++-------------------
3 files changed, 37 insertions(+), 21 deletions(-)
diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst
index 28041339d..b98516043 100644
--- a/docs/getting/tutorial.rst
+++ b/docs/getting/tutorial.rst
@@ -91,6 +91,26 @@ a ``Quantity()`` object.
``Quantity()`` objects also work well with NumPy arrays, which you can
read about in the section on :doc:`NumPy support `.
+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(pint.Quantity('1 pound * ft**2 * second**-2'))
+ '1.0 foot²·pound/second²'
+ >>> "{:P}".format(pint.Quantity('1 kg * cm**2 * second**-2'))
+ '1.0 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`):
+
+ >>> "{:P}".format(pint.Quantity('1 pound * ft**2 * second**-2'))
+ '1.0 pound·foot²/second²'
+ >>> "{:P}".format(pint.Quantity('1 kg * cm**2 * second**-2'))
+ '1.0 kilogram·centimeter²/second²'
+
+
+
Converting to different units
-----------------------------
@@ -180,11 +200,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
diff --git a/pint/default_en.txt b/pint/default_en.txt
index 391eeb37a..ed8e204f5 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -56,6 +56,7 @@
@defaults
group = international
system = mks
+ # This default order for sorting dimensions was described in the proposed ISO 80000 specification.
dim_order = [ "[substance]", "[mass]", "[current]", "[luminosity]", "[length]", "[]", "[time]", "[temperature]", ]
@end
diff --git a/pint/formatting.py b/pint/formatting.py
index b158da623..9b81209a3 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -14,7 +14,6 @@
import re
import warnings
from typing import Callable, Any, TYPE_CHECKING, TypeVar, List, Optional, Tuple, Union
-from collections import OrderedDict
from collections.abc import Iterable
from numbers import Number
@@ -189,6 +188,7 @@ def wrapper(func):
@register_unit_format("P")
def format_pretty(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
+ breakpoint()
return formatter(
unit.items(),
as_ratio=True,
@@ -223,18 +223,15 @@ def latex_escape(string: str) -> str:
def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
# Lift the sorting by dimensions b/c the preprocessed units are unrecognizeable
sorted_units = dim_sort(unit.items(), registry)
- preprocessed_list = [
+ preprocessed = [
(
rf"\mathrm{{{latex_escape(u)}}}",
p,
)
for u, p in sorted_units
]
- preprocessed = OrderedDict()
- for k, v in preprocessed_list:
- preprocessed[k] = v
formatted = formatter(
- preprocessed.items(),
+ preprocessed,
as_ratio=True,
single_denominator=True,
product_fmt=r" \cdot ",
@@ -311,7 +308,7 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
- """Sort a list of units by dimensional order.
+ """Sort a list of units by dimensional order (from `registry._defaults['dim_order']`).
Parameters
----------
@@ -332,17 +329,16 @@ def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
"""
if registry is None or len(items) <= 1:
return items
- # if len(items) == 2 and items[0][1] * items[1][1] < 0:
- # return items
ret_dict = dict()
- for name, value in items:
- cname = registry.get_name(name)
+ dim_order = registry._defaults["dim_order"]
+ for unit_name, unit_exponent in items:
+ cname = registry.get_name(unit_name)
if not cname:
continue
cname_dims = registry.get_dimensionality(cname)
if len(cname_dims) == 0:
cname_dims = {"[]": None}
- dim_types = iter(registry._defaults["dim_order"])
+ dim_types = iter(dim_order)
while True:
try:
dim = next(dim_types)
@@ -351,20 +347,17 @@ def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
ret_dict[dim] = list()
ret_dict[dim].append(
(
- name,
- value,
+ unit_name,
+ unit_exponent,
)
)
break
except StopIteration:
raise KeyError(
- f"Unit {name} (aka {cname}) has no recognized dimensions"
+ f"Unit {unit_name} (aka {cname}) has no recognized dimensions"
)
- ret = sum(
- [ret_dict[dim] for dim in registry._defaults["dim_order"] if dim in ret_dict],
- [],
- )
+ ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], [])
return ret
@@ -417,6 +410,8 @@ def formatter(
True to sort the units dimentionally (Default value = False).
When dimensions have multiple units, sort by "most significant dimension" the unit contains
When both `sort` and `sort_dims` are True, sort alphabetically within sorted dimensions
+ ISO 80000 and other sources guide on how dimensions shoule be ordered; the user
+ can set their preference in the registry.
registry : UnitRegistry, optional
The registry to use if `sort_dims` is True
From 58f2461016ee3c071ec72602a6bb665d3a73cec0 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 18:39:10 -0400
Subject: [PATCH 05/13] Update formatting.py
Remove `breakpoint()` left in by mistake.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
pint/formatting.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/pint/formatting.py b/pint/formatting.py
index 9b81209a3..82661f376 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -188,7 +188,6 @@ def wrapper(func):
@register_unit_format("P")
def format_pretty(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
- breakpoint()
return formatter(
unit.items(),
as_ratio=True,
From b4b461609fab62eb38471f0de664242f5eae2a21 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 18:48:32 -0400
Subject: [PATCH 06/13] Update tutorial.rst
More attempts to make doc text and doc build happy.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
docs/getting/tutorial.rst | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst
index b98516043..0f75f5c97 100644
--- a/docs/getting/tutorial.rst
+++ b/docs/getting/tutorial.rst
@@ -95,19 +95,23 @@ 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(pint.Quantity('1 pound * ft**2 * second**-2'))
- '1.0 foot²·pound/second²'
- >>> "{:P}".format(pint.Quantity('1 kg * cm**2 * second**-2'))
- '1.0 centimeter²·kilogram/second²'
+.. doctest::
+
+ >>> "{:P}".format(ureg.parse_units('pound * ft**2 * second**-2'))
+ 'foot²·pound/second²'
+ >>> "{:P}".format(ureg.parse_units('kg * cm**2 * second**-2'))
+ '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`):
- >>> "{:P}".format(pint.Quantity('1 pound * ft**2 * second**-2'))
- '1.0 pound·foot²/second²'
- >>> "{:P}".format(pint.Quantity('1 kg * cm**2 * second**-2'))
- '1.0 kilogram·centimeter²/second²'
+.. 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²'
From 45a39097fc675e564b1f235380674ebb6c35be95 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 18:59:03 -0400
Subject: [PATCH 07/13] Update tutorial.rst
See whether I've turned off doctest where old behavior is no longer easily reproducible.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
docs/getting/tutorial.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst
index 0f75f5c97..e3c25fe44 100644
--- a/docs/getting/tutorial.rst
+++ b/docs/getting/tutorial.rst
@@ -95,12 +95,12 @@ 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):
-.. doctest::
-
+```ignore
>>> "{:P}".format(ureg.parse_units('pound * ft**2 * second**-2'))
'foot²·pound/second²'
>>> "{:P}".format(ureg.parse_units('kg * cm**2 * second**-2'))
'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
From 69f947493c3ebdd261110275bcf1e042bf4627c6 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Sun, 22 Oct 2023 19:02:31 -0400
Subject: [PATCH 08/13] Update tutorial.rst
And another attempt.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
docs/getting/tutorial.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst
index e3c25fe44..d0b9347bf 100644
--- a/docs/getting/tutorial.rst
+++ b/docs/getting/tutorial.rst
@@ -95,10 +95,10 @@ 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):
-```ignore
- >>> "{:P}".format(ureg.parse_units('pound * ft**2 * second**-2'))
+```
+ "{: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'))
+ "{:P}".format(ureg.parse_units('kg * cm**2 * second**-2')) would yield:
'centimeter²·kilogram/second²'
```
From 271197096bc8f29216e598830471f9c15efcb4eb Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Thu, 11 Jan 2024 14:35:19 +1300
Subject: [PATCH 09/13] Update __init__.py
Prepare to merge `develop` into this PR.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
pint/delegates/formatter/__init__.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pint/delegates/formatter/__init__.py b/pint/delegates/formatter/__init__.py
index c30f3657b..55b44b31e 100644
--- a/pint/delegates/formatter/__init__.py
+++ b/pint/delegates/formatter/__init__.py
@@ -8,9 +8,10 @@
from .base_formatter import BaseFormatter
+from .iso80000_formatter import ISO80000Formatter
-class Formatter(BaseFormatter):
+class Formatter(ISO80000Formatter, BaseFormatter):
# TODO: this should derive from all relevant formaters to
# reproduce the current behavior of Pint.
pass
From f9e9dbad74cf7a8da00c064ec639397f19f0ed2c Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Mon, 15 Jan 2024 17:28:09 +1300
Subject: [PATCH 10/13] Add dim_order to BaseFormatter
Move the proposed `dim_sort` functionality from being a system registry property to being a formatter property, specifically `BaseFormatter`.
Also fix typos noticed in `pint/testsuite/conftest.py`.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
CHANGES | 2 +-
pint/default_en.txt | 2 --
pint/delegates/formatter/__init__.py | 5 ++---
pint/delegates/formatter/base_formatter.py | 3 +++
pint/facets/plain/definitions.py | 5 +----
pint/formatting.py | 4 ++--
pint/testsuite/conftest.py | 6 +++---
7 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/CHANGES b/CHANGES
index 01f5644c0..2b0754435 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,9 +4,9 @@ Pint Changelog
0.24 (unreleased)
-----------------
+- Add `dim_order` property to BaseFormatter.
- Add `dim_sort` parameter to formatter.
(PR #1864, fixes Issue #1841)
-- Nothing changed yet.
0.23 (2023-12-08)
diff --git a/pint/default_en.txt b/pint/default_en.txt
index ed8e204f5..5fc7f8265 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -56,8 +56,6 @@
@defaults
group = international
system = mks
- # This default order for sorting dimensions was described in the proposed ISO 80000 specification.
- dim_order = [ "[substance]", "[mass]", "[current]", "[luminosity]", "[length]", "[]", "[time]", "[temperature]", ]
@end
diff --git a/pint/delegates/formatter/__init__.py b/pint/delegates/formatter/__init__.py
index 707abc677..814156afb 100644
--- a/pint/delegates/formatter/__init__.py
+++ b/pint/delegates/formatter/__init__.py
@@ -7,11 +7,10 @@
"""
-from .base_formatter import BabelFormatter
-from .iso80000_formatter import ISO80000Formatter
+from .base_formatter import BaseFormatter, BabelFormatter
-class Formatter(ISO80000Formatter, BabelFormatter):
+class Formatter(BabelFormatter, BaseFormatter):
# TODO: this should derive from all relevant formaters to
# reproduce the current behavior of Pint.
pass
diff --git a/pint/delegates/formatter/base_formatter.py b/pint/delegates/formatter/base_formatter.py
index d15a7a6a6..d618b2140 100644
--- a/pint/delegates/formatter/base_formatter.py
+++ b/pint/delegates/formatter/base_formatter.py
@@ -34,6 +34,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:
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index 05cb055a0..c32637e75 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -14,7 +14,7 @@
import ast
from dataclasses import dataclass
from functools import cached_property
-from typing import Any, List, Optional
+from typing import Any, Optional
from ..._typing import Magnitude
from ... import errors
@@ -61,15 +61,12 @@ class DefaultsDefinition:
group: ty.Optional[str]
system: ty.Optional[str]
- dim_order: ty.Optional[List[str]]
def items(self):
if self.group is not None:
yield "group", self.group
if self.system is not None:
yield "system", self.system
- if self.dim_order is not None:
- yield "dim_order", ast.literal_eval(self.dim_order)
@dataclass(frozen=True)
diff --git a/pint/formatting.py b/pint/formatting.py
index 82661f376..57846a88d 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -307,7 +307,7 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
- """Sort a list of units by dimensional order (from `registry._defaults['dim_order']`).
+ """Sort a list of units by dimensional order (from `registry.formatter.dim_order`).
Parameters
----------
@@ -329,7 +329,7 @@ def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry):
if registry is None or len(items) <= 1:
return items
ret_dict = dict()
- dim_order = registry._defaults["dim_order"]
+ dim_order = registry.formatter.dim_order
for unit_name, unit_exponent in items:
cname = registry.get_name(unit_name)
if not cname:
diff --git a/pint/testsuite/conftest.py b/pint/testsuite/conftest.py
index d51bc8c05..201e56a0e 100644
--- a/pint/testsuite/conftest.py
+++ b/pint/testsuite/conftest.py
@@ -70,19 +70,19 @@ def func_registry():
@pytest.fixture(scope="class")
def class_registry():
- """Only use for those test that do not modify the registry."""
+ """Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()
@pytest.fixture(scope="module")
def module_registry():
- """Only use for those test that do not modify the registry."""
+ """Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()
@pytest.fixture(scope="session")
def sess_registry():
- """Only use for those test that do not modify the registry."""
+ """Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()
From 0b049656a59788202c3ed4236eddbbc5ad6b783e Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Mon, 15 Jan 2024 17:33:07 +1300
Subject: [PATCH 11/13] Make black and ruff happy.
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
pint/delegates/formatter/base_formatter.py | 11 ++++++++++-
pint/facets/plain/definitions.py | 1 -
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/pint/delegates/formatter/base_formatter.py b/pint/delegates/formatter/base_formatter.py
index d618b2140..f0db6c5ff 100644
--- a/pint/delegates/formatter/base_formatter.py
+++ b/pint/delegates/formatter/base_formatter.py
@@ -35,7 +35,16 @@
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]", )
+ dim_order = (
+ "[substance]",
+ "[mass]",
+ "[current]",
+ "[luminosity]",
+ "[length]",
+ "[]",
+ "[time]",
+ "[temperature]",
+ )
def format_quantity(
self, quantity: PlainQuantity[MagnitudeT], spec: str = ""
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index c32637e75..44bf29858 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -11,7 +11,6 @@
import itertools
import numbers
import typing as ty
-import ast
from dataclasses import dataclass
from functools import cached_property
from typing import Any, Optional
From 41dcc421108fab67a1c41a6578fe242b516dbeaa Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Mon, 15 Jan 2024 17:38:28 +1300
Subject: [PATCH 12/13] Update requirements_docs.txt
Based on this error message from the CI build:
```
Run sphinx-build -n -j auto -b html -d build/doctrees docs build/html
sphinx-build -n -j auto -b html -d build/doctrees docs build/html
shell: /usr/bin/bash -e {0}
env:
pythonLocation: /opt/hostedtoolcache/Python/3.9.18/x64
LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.9.18/x64/lib
Running Sphinx v4.5.0
Sphinx version error:
The sphinxcontrib.applehelp extension used by this project needs at least Sphinx v5.0; it therefore cannot be built with this version.
Error: Process completed with exit code 2.
```
bump sphinx to >= 5 (>4 only got us to 4.5).
Signed-off-by: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
---
requirements_docs.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 8f4410960..68a96efb1 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,4 +1,4 @@
-sphinx>4
+sphinx>=5
ipython<=8.12
matplotlib
mip>=1.13
From 957e9ca61d6e2e5f3217728ede059234ee9c3b30 Mon Sep 17 00:00:00 2001
From: Michael Tiemann <72577720+MichaelTiemannOSC@users.noreply.github.com>
Date: Mon, 15 Jan 2024 17:40:41 +1300
Subject: [PATCH 13/13] Revert "Update requirements_docs.txt"
This reverts commit 41dcc421108fab67a1c41a6578fe242b516dbeaa.
---
requirements_docs.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 68a96efb1..8f4410960 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,4 +1,4 @@
-sphinx>=5
+sphinx>4
ipython<=8.12
matplotlib
mip>=1.13