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