From b326660f283b63ea87f8841d8cf1043c87d90c79 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Fri, 8 Nov 2024 01:43:01 -0300 Subject: [PATCH 01/16] refactor: reorganize and add typing to pint/pint_eval.py --- pint/pint_eval.py | 157 ++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git a/pint/pint_eval.py b/pint/pint_eval.py index c2ddb29cd..8c5f30e31 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -12,40 +12,24 @@ import operator import token as tokenlib import tokenize +from collections.abc import Iterable from io import BytesIO from tokenize import TokenInfo -from typing import Any - -try: - from uncertainties import ufloat - - HAS_UNCERTAINTIES = True -except ImportError: - HAS_UNCERTAINTIES = False - ufloat = None +from typing import Any, Callable, Generator, Generic, Iterator, TypeVar +from .compat import HAS_UNCERTAINTIES, ufloat from .errors import DefinitionSyntaxError -# For controlling order of operations -_OP_PRIORITY = { - "+/-": 4, - "**": 3, - "^": 3, - "unary": 2, - "*": 1, - "": 1, # operator for implicit ops - "//": 1, - "/": 1, - "%": 1, - "+": 0, - "-": 0, -} +S = TypeVar("S") +if HAS_UNCERTAINTIES: + _ufloat = ufloat # type: ignore +else: -def _ufloat(left, right): - if HAS_UNCERTAINTIES: - return ufloat(left, right) - raise TypeError("Could not import support for uncertainties") + def _ufloat(*args: Any, **kwargs: Any): + raise TypeError( + "Please install the uncertainties package to be able to parse quantities with uncertainty." + ) def _power(left: Any, right: Any) -> Any: @@ -63,46 +47,93 @@ def _power(left: Any, right: Any) -> Any: return operator.pow(left, right) -# https://stackoverflow.com/a/1517965/1291237 -class tokens_with_lookahead: - def __init__(self, iter): +UnaryOpT = Callable[ + [ + Any, + ], + Any, +] +BinaryOpT = Callable[[Any, Any], Any] + +_UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1} + +_BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = { + "+/-": _ufloat, + "**": _power, + "*": operator.mul, + "": operator.mul, # operator for implicit ops + "/": operator.truediv, + "+": operator.add, + "-": operator.sub, + "%": operator.mod, + "//": operator.floordiv, +} + +# For controlling order of operations +_OP_PRIORITY = { + "+/-": 4, + "**": 3, + "^": 3, + "unary": 2, + "*": 1, + "": 1, # operator for implicit ops + "//": 1, + "/": 1, + "%": 1, + "+": 0, + "-": 0, +} + + +class IteratorLookAhead(Generic[S]): + """An iterator with lookahead buffer. + + Adapted: https://stackoverflow.com/a/1517965/1291237 + """ + + def __init__(self, iter: Iterator[S]): self.iter = iter - self.buffer = [] + self.buffer: list[S] = [] def __iter__(self): return self - def __next__(self): + def __next__(self) -> S: if self.buffer: return self.buffer.pop(0) else: return self.iter.__next__() - def lookahead(self, n): + def lookahead(self, n: int) -> S: """Return an item n entries ahead in the iteration.""" while n >= len(self.buffer): try: self.buffer.append(self.iter.__next__()) except StopIteration: - return None + raise ValueError("Cannot look ahead, out of range") return self.buffer[n] -def _plain_tokenizer(input_string): +def plain_tokenizer(input_string: str) -> Generator[TokenInfo, None, None]: + """Standard python tokenizer""" for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline): if tokinfo.type != tokenlib.ENCODING: yield tokinfo -def uncertainty_tokenizer(input_string): - def _number_or_nan(token): +def uncertainty_tokenizer(input_string: str) -> Generator[TokenInfo, None, None]: + """Tokenizer capable of parsing uncertainties as v+/-u and v±u""" + + def _number_or_nan(token: TokenInfo) -> bool: if token.type == tokenlib.NUMBER or ( token.type == tokenlib.NAME and token.string == "nan" ): return True return False - def _get_possible_e(toklist, e_index): + def _get_possible_e( + toklist: IteratorLookAhead[TokenInfo], e_index: int + ) -> TokenInfo | None: possible_e_token = toklist.lookahead(e_index) if ( possible_e_token.string[0] == "e" @@ -143,7 +174,7 @@ def _get_possible_e(toklist, e_index): possible_e = None return possible_e - def _apply_e_notation(mantissa, exponent): + def _apply_e_notation(mantissa: TokenInfo, exponent: TokenInfo) -> TokenInfo: if mantissa.string == "nan": return mantissa if float(mantissa.string) == 0.0: @@ -156,7 +187,12 @@ def _apply_e_notation(mantissa, exponent): line=exponent.line, ) - def _finalize_e(nominal_value, std_dev, toklist, possible_e): + def _finalize_e( + nominal_value: TokenInfo, + std_dev: TokenInfo, + toklist: IteratorLookAhead[TokenInfo], + possible_e: TokenInfo, + ) -> tuple[TokenInfo, TokenInfo]: nominal_value = _apply_e_notation(nominal_value, possible_e) std_dev = _apply_e_notation(std_dev, possible_e) next(toklist) # consume 'e' and positive exponent value @@ -178,8 +214,9 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e): # wading through all that vomit, just eliminate the problem # in the input by rewriting ± as +/-. input_string = input_string.replace("±", "+/-") - toklist = tokens_with_lookahead(_plain_tokenizer(input_string)) + toklist = IteratorLookAhead(plain_tokenizer(input_string)) for tokinfo in toklist: + assert tokinfo is not None line = tokinfo.line start = tokinfo.start if ( @@ -194,7 +231,7 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e): end=toklist.lookahead(1).end, line=line, ) - for i in range(-1, 1): + for _ in range(-1, 1): next(toklist) yield plus_minus_op elif ( @@ -280,31 +317,7 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e): if HAS_UNCERTAINTIES: tokenizer = uncertainty_tokenizer else: - tokenizer = _plain_tokenizer - -import typing - -UnaryOpT = typing.Callable[ - [ - Any, - ], - Any, -] -BinaryOpT = typing.Callable[[Any, Any], Any] - -_UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1} - -_BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = { - "+/-": _ufloat, - "**": _power, - "*": operator.mul, - "": operator.mul, # operator for implicit ops - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, - "%": operator.mod, - "//": operator.floordiv, -} + tokenizer = plain_tokenizer class EvalTreeNode: @@ -344,12 +357,7 @@ def to_string(self) -> str: def evaluate( self, - define_op: typing.Callable[ - [ - Any, - ], - Any, - ], + define_op: UnaryOpT, bin_op: dict[str, BinaryOpT] | None = None, un_op: dict[str, UnaryOpT] | None = None, ): @@ -395,9 +403,6 @@ def evaluate( return define_op(self.left) -from collections.abc import Iterable - - def _build_eval_tree( tokens: list[TokenInfo], op_priority: dict[str, int], From f05be7d22d0bfc6d9a0312a95c3d91eb055d2025 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 15 Feb 2025 00:11:26 -0300 Subject: [PATCH 02/16] fix: now _plain_tokenizer is plain_tokenizer --- pint/testsuite/benchmarks/test_01_eval.py | 3 +-- pint/testsuite/test_pint_eval.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pint/testsuite/benchmarks/test_01_eval.py b/pint/testsuite/benchmarks/test_01_eval.py index b3f35d2a0..70f5d85a8 100644 --- a/pint/testsuite/benchmarks/test_01_eval.py +++ b/pint/testsuite/benchmarks/test_01_eval.py @@ -2,8 +2,7 @@ import pytest -from pint.pint_eval import _plain_tokenizer as plain_tokenizer -from pint.pint_eval import uncertainty_tokenizer +from pint.pint_eval import plain_tokenizer, uncertainty_tokenizer VALUES = [ "1", diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index f8d72bb5e..09433d133 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -2,8 +2,7 @@ import pytest -from pint.pint_eval import _plain_tokenizer as plain_tokenizer -from pint.pint_eval import build_eval_tree, uncertainty_tokenizer +from pint.pint_eval import build_eval_tree, plain_tokenizer, uncertainty_tokenizer from pint.util import string_preprocessor TOKENIZERS = (plain_tokenizer, uncertainty_tokenizer) From 7f9473aac3b390518ec4153081853916e20de8f3 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:00:34 -0300 Subject: [PATCH 03/16] test: remove test_issue39 as np.matrix is deprecated --- pint/testsuite/test_issues.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 8501661d0..698e16cce 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -70,29 +70,6 @@ def test_issue37(self, module_registry): np.testing.assert_array_equal(qq.magnitude, x * m) assert qq.units == module_registry.meter.units - @pytest.mark.xfail - @helpers.requires_numpy - def test_issue39(self, module_registry): - x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) - q = module_registry.meter * x - assert isinstance(q, module_registry.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - assert q.units == module_registry.meter.units - q = x * module_registry.meter - assert isinstance(q, module_registry.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - assert q.units == module_registry.meter.units - - m = np.matrix(2 * np.ones(3, 3)) - qq = q * m - assert isinstance(qq, module_registry.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - assert qq.units == module_registry.meter.units - qq = m * q - assert isinstance(qq, module_registry.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - assert qq.units == module_registry.meter.units - @helpers.requires_numpy def test_issue44(self, module_registry): x = 4.0 * module_registry.dimensionless From 607bdeeeda151277148b998d435402a064f4f78b Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:05:10 -0300 Subject: [PATCH 04/16] test: missing number multiplying permille in test_issue1963 --- pint/testing.py | 10 ++++++++-- pint/testsuite/test_issues.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pint/testing.py b/pint/testing.py index c5508d8d2..7cbb2c67a 100644 --- a/pint/testing.py +++ b/pint/testing.py @@ -73,10 +73,16 @@ def assert_equal(first, second, msg: str | None = None) -> None: if isinstance(m1, ndarray) or isinstance(m2, ndarray): np.testing.assert_array_equal(m1, m2, err_msg=msg) elif not isinstance(m1, Number): - warnings.warn("In assert_equal, m1 is not a number ", UserWarning) + warnings.warn( + f"In assert_equal, m1 is not a number {first} ({m1}) vs. {second} ({m2}) ", + UserWarning, + ) return elif not isinstance(m2, Number): - warnings.warn("In assert_equal, m2 is not a number ", UserWarning) + warnings.warn( + f"In assert_equal, m2 is not a number {first} ({m1}) vs. {second} ({m2}) ", + UserWarning, + ) return elif math.isnan(m1): assert math.isnan(m2), msg diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 698e16cce..d8229b1e7 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -886,7 +886,7 @@ def test_issue1963(self, module_registry): assert_equal(1e2 * b, a) assert_equal(c, 50 * a) - assert_equal((1 * ureg.milligram) / (1 * ureg.gram), ureg.permille) + assert_equal((1 * ureg.milligram) / (1 * ureg.gram), 1 * ureg.permille) @pytest.mark.xfail @helpers.requires_uncertainties() From 1e45a60b3ff2bda30a6df2f40f51b0e7a53d31b1 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:10:56 -0300 Subject: [PATCH 05/16] test: numpy.trapz is deprectated in favour of numpy.trapezoid --- pint/testsuite/test_numpy.py | 2 +- pint/testsuite/test_numpy_func.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 3075be7ac..4784d852b 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -442,7 +442,7 @@ def test_cross(self): @helpers.requires_array_function_protocol() def test_trapz(self): helpers.assert_quantity_equal( - np.trapz([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), + np.trapezoid([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), 7.5 * self.ureg.J * self.ureg.m, ) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 9c69a238d..934efe09f 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -207,14 +207,14 @@ def test_trapz(self): t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") helpers.assert_quantity_equal( - np.trapz(t, x=z), self.Q_(1108.6, "kelvin meter") + np.trapezoid(t, x=z), self.Q_(1108.6, "kelvin meter") ) def test_trapz_no_autoconvert(self): t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") with pytest.raises(OffsetUnitCalculusError): - np.trapz(t, x=z) + np.trapezoid(t, x=z) def test_correlate(self): a = self.Q_(np.array([1, 2, 3]), "m") From 6fc74c6e529d4f082e1c53e71c7654d02005075b Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:15:23 -0300 Subject: [PATCH 06/16] test: TestQuantityToCompact::test_nonnumeric_magnitudes should call to_compact, not compare --- pint/testing.py | 10 ++++++++-- pint/testsuite/test_quantity.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pint/testing.py b/pint/testing.py index 7cbb2c67a..b1da02935 100644 --- a/pint/testing.py +++ b/pint/testing.py @@ -137,10 +137,16 @@ def assert_allclose( if isinstance(m1, ndarray) or isinstance(m2, ndarray): np.testing.assert_allclose(m1, m2, rtol=rtol, atol=atol, err_msg=msg) elif not isinstance(m1, Number): - warnings.warn("In assert_equal, m1 is not a number ", UserWarning) + warnings.warn( + f"In assert_equal, m1 is not a number {first} ({m1}) vs. {second} ({m2}) ", + UserWarning, + ) return elif not isinstance(m2, Number): - warnings.warn("In assert_equal, m2 is not a number ", UserWarning) + warnings.warn( + f"In assert_equal, m1 is not a number {first} ({m1}) vs. {second} ({m2}) ", + UserWarning, + ) return elif math.isnan(m1): assert math.isnan(m2), msg diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 6f173216c..5fc397b12 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -842,7 +842,7 @@ def test_nonnumeric_magnitudes(self): ureg = self.ureg x = "some string" * ureg.m with pytest.warns(UndefinedBehavior): - self.compare_quantity_compact(x, x) + x.to_compact() def test_very_large_to_compact(self): # This should not raise an IndexError From d020e55065e328e96482c10f8f236f8905b9db93 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:30:47 -0300 Subject: [PATCH 07/16] test: upgrade to new formatter delegate --- pint/testsuite/test_babel.py | 2 +- pint/testsuite/test_issues.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index c68c641e7..ee8e4bb42 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -13,7 +13,7 @@ def test_no_babel(func_registry): ureg = func_registry distance = 24.0 * ureg.meter with pytest.raises(Exception): - distance.format_babel(locale="fr_FR", length="long") + ureg.formatter.format_unit_babel(distance, locale="fr_FR", length="long") @helpers.requires_babel(["fr_FR", "ro_RO"]) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index d8229b1e7..ae77fd18c 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1207,7 +1207,7 @@ def test_issue_1845(): def test_issues_1841(func_registry, units, spec, expected): ur = func_registry ur.formatter.default_sort_func = sort_by_dimensionality - ur.default_format = spec + ur.formatter.default_format = spec value = ur.Unit(UnitsContainer(**units)) assert f"{value}" == expected @@ -1219,7 +1219,7 @@ def test_issues_1841_xfail(): # sets compact display mode by default ur = UnitRegistry() - ur.default_format = "~P" + ur.formatter.default_format = "~P" ur.formatter.default_sort_func = sort_by_dimensionality q = ur.Quantity("2*pi radian * hour") From 5585dd8bd94a445a6c28e5021be3b8b8ac5d98d3 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 9 Nov 2024 23:45:43 -0300 Subject: [PATCH 08/16] test: UnitStrippedWarning is expected when copyto a non-quantity --- pint/testsuite/test_numpy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 4784d852b..705adb47f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1227,8 +1227,10 @@ def test_copyto(self): helpers.assert_quantity_equal(q, self.Q_([[2, 2], [6, 4]], "m")) np.copyto(q, 0, where=[[False, False], [True, False]]) helpers.assert_quantity_equal(q, self.Q_([[2, 2], [0, 4]], "m")) - np.copyto(a, q) - self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) + with pytest.warns(UnitStrippedWarning): + # as a is not quantity, the unit is stripped. + np.copyto(a, q) + self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) @helpers.requires_array_function_protocol() def test_tile(self): From 1b718d09c33ffdc612a85605723b0590b28428e2 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sun, 10 Nov 2024 00:50:49 -0300 Subject: [PATCH 09/16] test: test should fail when xfail/xpassed is not working as expected --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 702c85865..ecb00df74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ cache-keys = [{ file = "pyproject.toml" }, { git = true }] [tool.pytest.ini_options] addopts = "--import-mode=importlib" +xfail_strict = true pythonpath = "." [tool.ruff.format] From 7f41d75fd23c07250b2d1457cb98563c161f1c2e Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Wed, 13 Nov 2024 23:49:05 -0300 Subject: [PATCH 10/16] test: remove print from test --- pint/testsuite/test_measurement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 8f20deead..a66f72dc1 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -296,5 +296,5 @@ def test_tokenization(self): pint_eval.tokenizer = pint_eval.uncertainty_tokenizer for p in pint_eval.tokenizer("8 + / - 4"): - print(p) + str(p) assert True From 6a72c20e513c8798cf79163c5b99807f582ed688 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Wed, 13 Nov 2024 23:49:24 -0300 Subject: [PATCH 11/16] test: remove print from test --- pint/testsuite/test_issues.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index ae77fd18c..010074dde 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1274,7 +1274,6 @@ def test_issue2017(): @fmt.register_unit_format("test") def _test_format(unit, registry, **options): - print("format called") proc = {u.replace("µ", "u"): e for u, e in unit.items()} return fmt.formatter( proc.items(), From bdbd50267153bc0cecb82b9339c9a1a22bd0aadd Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sun, 10 Nov 2024 00:50:17 -0300 Subject: [PATCH 12/16] test: xfail is incorrect here --- pint/testsuite/test_quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 5fc397b12..3bc6dce31 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -273,13 +273,13 @@ def test_default_formatting(self, subtests): ureg.formatter.default_format = spec assert f"{x}" == result - @pytest.mark.xfail(reason="Still not clear how default formatting will work.") def test_formatting_override_default_units(self): ureg = UnitRegistry() ureg.formatter.default_format = "~" x = ureg.Quantity(4, "m ** 2") assert f"{x:dP}" == "4 meter²" + ureg.separate_format_defaults = None with pytest.warns(DeprecationWarning): assert f"{x:d}" == "4 meter ** 2" @@ -287,13 +287,13 @@ def test_formatting_override_default_units(self): with assert_no_warnings(): assert f"{x:d}" == "4 m ** 2" - @pytest.mark.xfail(reason="Still not clear how default formatting will work.") def test_formatting_override_default_magnitude(self): ureg = UnitRegistry() ureg.formatter.default_format = ".2f" x = ureg.Quantity(4, "m ** 2") assert f"{x:dP}" == "4 meter²" + ureg.separate_format_defaults = None with pytest.warns(DeprecationWarning): assert f"{x:D}" == "4 meter ** 2" From c9f7e91db0d1279eca72a3283e4a0190e021792a Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sun, 10 Nov 2024 00:47:43 -0300 Subject: [PATCH 13/16] fix: split split_format to keep lru_cache but use warning every time --- pint/delegates/formatter/_spec_helpers.py | 62 ++++++++++++++--------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/pint/delegates/formatter/_spec_helpers.py b/pint/delegates/formatter/_spec_helpers.py index 344859b38..5f52b5ee0 100644 --- a/pint/delegates/formatter/_spec_helpers.py +++ b/pint/delegates/formatter/_spec_helpers.py @@ -1,11 +1,11 @@ """ - pint.delegates.formatter._spec_helpers - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +pint.delegates.formatter._spec_helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Convenient functions to deal with format specifications. +Convenient functions to deal with format specifications. - :copyright: 2022 by Pint Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. +:copyright: 2022 by Pint Authors, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -87,10 +87,18 @@ def remove_custom_flags(spec: str) -> str: return spec +########## +# This weird way of defining split format +# is the only reasonable way I foudn to use +# lru_cache in a function that might emit warning +# and do it every time. +# TODO: simplify it when there are no warnings. + + @functools.lru_cache -def split_format( +def _split_format( spec: str, default: str, separate_format_defaults: bool = True -) -> tuple[str, str]: +) -> tuple[str, str, list[str]]: """Split format specification into magnitude and unit format.""" mspec = remove_custom_flags(spec) uspec = extract_custom_flags(spec) @@ -98,29 +106,24 @@ def split_format( default_mspec = remove_custom_flags(default) default_uspec = extract_custom_flags(default) + warns = [] if separate_format_defaults in (False, None): # should we warn always or only if there was no explicit choice? # Given that we want to eventually remove the flag again, I'd say yes? if spec and separate_format_defaults is None: if not uspec and default_uspec: - warnings.warn( - ( - "The given format spec does not contain a unit formatter." - " Falling back to the builtin defaults, but in the future" - " the unit formatter specified in the `default_format`" - " attribute will be used instead." - ), - DeprecationWarning, + warns.append( + "The given format spec does not contain a unit formatter." + " Falling back to the builtin defaults, but in the future" + " the unit formatter specified in the `default_format`" + " attribute will be used instead." ) if not mspec and default_mspec: - warnings.warn( - ( - "The given format spec does not contain a magnitude formatter." - " Falling back to the builtin defaults, but in the future" - " the magnitude formatter specified in the `default_format`" - " attribute will be used instead." - ), - DeprecationWarning, + warns.append( + "The given format spec does not contain a magnitude formatter." + " Falling back to the builtin defaults, but in the future" + " the magnitude formatter specified in the `default_format`" + " attribute will be used instead." ) elif not spec: mspec, uspec = default_mspec, default_uspec @@ -128,4 +131,17 @@ def split_format( mspec = mspec or default_mspec uspec = uspec or default_uspec + return mspec, uspec, warns + + +def split_format( + spec: str, default: str, separate_format_defaults: bool = True +) -> tuple[str, str]: + """Split format specification into magnitude and unit format.""" + + mspec, uspec, warns = _split_format(spec, default, separate_format_defaults) + + for warn_msg in warns: + warnings.warn(warn_msg, DeprecationWarning) + return mspec, uspec From 9b3320314fc7a136efb8e2a53f178d1ed8fdef5b Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 15 Feb 2025 00:56:23 -0300 Subject: [PATCH 14/16] test: trapezoid should be used for Numpy >= 2 and trapz otherwise --- pint/testsuite/test_numpy_func.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 934efe09f..141fa44aa 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -210,6 +210,7 @@ def test_trapz(self): np.trapezoid(t, x=z), self.Q_(1108.6, "kelvin meter") ) + @helpers.requires_numpy_at_least("2.0") def test_trapz_no_autoconvert(self): t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") From 48106e2fd082033d9aa045d9f501de07781ed5d9 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 15 Feb 2025 01:02:10 -0300 Subject: [PATCH 15/16] test: trapz should be used for Numpy < 2 --- pint/testsuite/test_numpy_func.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 141fa44aa..4a6786f43 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -210,8 +210,15 @@ def test_trapz(self): np.trapezoid(t, x=z), self.Q_(1108.6, "kelvin meter") ) - @helpers.requires_numpy_at_least("2.0") + @helpers.requires_numpy_previous_than("2.0") def test_trapz_no_autoconvert(self): + t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") + z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") + with pytest.raises(OffsetUnitCalculusError): + np.trapz(t, x=z) + + @helpers.requires_numpy_at_least("2.0") + def test_trapezoid_no_autoconvert(self): t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") with pytest.raises(OffsetUnitCalculusError): From d7c69f798bc6702d9380468dd35c2389d90306f7 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Sat, 15 Feb 2025 01:15:57 -0300 Subject: [PATCH 16/16] test: trapezoid should be used for Numpy >= 2 and trapz otherwise --- pint/testsuite/test_numpy.py | 3 ++- pint/testsuite/test_numpy_func.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 705adb47f..722dcc4f5 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -440,9 +440,10 @@ def test_cross(self): # NP2: Remove this when we only support np>=2.0 @helpers.requires_array_function_protocol() + @helpers.requires_numpy_previous_than("2.0") def test_trapz(self): helpers.assert_quantity_equal( - np.trapezoid([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), + np.trapz([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), 7.5 * self.ureg.J * self.ureg.m, ) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 4a6786f43..28fa0d121 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -195,7 +195,24 @@ def test_numpy_wrap(self): # TODO (#905 follow-up): test that NotImplemented is returned when upcast types # present + @helpers.requires_numpy_previous_than("2.0") def test_trapz(self): + with ExitStack() as stack: + stack.callback( + setattr, + self.ureg, + "autoconvert_offset_to_baseunit", + self.ureg.autoconvert_offset_to_baseunit, + ) + self.ureg.autoconvert_offset_to_baseunit = True + t = self.Q_(np.array([0.0, 4.0, 8.0]), "degC") + z = self.Q_(np.array([0.0, 2.0, 4.0]), "m") + helpers.assert_quantity_equal( + np.trapz(t, x=z), self.Q_(1108.6, "kelvin meter") + ) + + @helpers.requires_numpy_at_least("2.0") + def test_trapezoid(self): with ExitStack() as stack: stack.callback( setattr,