From 079b35afb87fabe5180ba9fe14932e755c014373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Sat, 15 Jan 2022 17:43:31 +0100 Subject: [PATCH 1/9] Fix 1427: Keep track of exponent reduction --- pint/quantity.py | 2 ++ pint/testsuite/test_quantity.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index 138e17aa6..cfc5d93bf 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -771,6 +771,8 @@ def _get_reduced_units(self, units): if unit1 not in units: continue for unit2 in units: + # get exponent after reduction + exp = units[unit1] if unit1 != unit2: power = self._REGISTRY._get_dimensionality_ratio(unit1, unit2) if power: diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index b5413f4c1..998a53daa 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -642,6 +642,9 @@ def test_to_reduced_units(self): q.to_reduced_units(), self.Q_([3000.0, 4000.0], "ms**2") ) + q = self.Q_(0.5, "g*t/kg") + helpers.assert_quantity_equal(q.to_reduced_units(), self.Q_(0.5, "kg")) + class TestQuantityToCompact(QuantityTestCase): def assertQuantityAlmostIdentical(self, q1, q2): From 18fb0b214179a897a5fb31da8f73f8817546cf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Sun, 23 Jan 2022 22:15:13 +0100 Subject: [PATCH 2/9] Use default numpy `np.printoptions` available since numpy 1.15 --- CHANGES | 1 + pint/quantity.py | 21 ++++----------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/CHANGES b/CHANGES index 64d523394..1dac418b9 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Pint Changelog - Allow Quantity to parse 'NaN' and 'inf(inity)', case insensitive - Fix casting error when using to_reduced_units with array of int. (Issue #1184) +- Use default numpy `np.printoptions` available since numpy 1.15. 0.18 (2021-10-26) diff --git a/pint/quantity.py b/pint/quantity.py index 138e17aa6..f79f1a94e 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -9,7 +9,6 @@ from __future__ import annotations import bisect -import contextlib import copy import datetime import functools @@ -168,20 +167,6 @@ def wrapper(self, *args, **kwargs): return wrapper -@contextlib.contextmanager -def printoptions(*args, **kwargs): - """Numpy printoptions context manager released with version 1.15.0 - https://docs.scipy.org/doc/numpy/reference/generated/numpy.printoptions.html - """ - - opts = np.get_printoptions() - try: - np.set_printoptions(*args, **kwargs) - yield np.get_printoptions() - finally: - np.set_printoptions(**opts) - - # Workaround to bypass dynamically generated Quantity with overload method Magnitude = TypeVar("Magnitude") @@ -413,7 +398,9 @@ def __format__(self, spec: str) -> str: allf = plain_allf = "{} {}" mstr = formatter.format(obj.magnitude) else: - with printoptions(formatter={"float_kind": formatter.format}): + with np.printoptions( + formatter={"float_kind": formatter.format} + ): mstr = ( "
"
                                 + format(obj.magnitude).replace("\n", "
") @@ -440,7 +427,7 @@ def __format__(self, spec: str) -> str: if obj.magnitude.ndim == 0: mstr = formatter.format(obj.magnitude) else: - with printoptions(formatter={"float_kind": formatter.format}): + with np.printoptions(formatter={"float_kind": formatter.format}): mstr = format(obj.magnitude).replace("\n", "") else: mstr = format(obj.magnitude, mspec).replace("\n", "") From 15818d342e6599458944745377a29805403c6b5c Mon Sep 17 00:00:00 2001 From: Eltos Date: Tue, 1 Feb 2022 17:39:32 +0100 Subject: [PATCH 3/9] Create unittest to check for default formats --- pint/testsuite/test_measurement.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 323140fd5..61427cdba 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -156,6 +156,28 @@ def test_format_exponential_neg(self, subtests): with subtests.test(spec): assert spec.format(m) == result + def test_format_default(self, subtests): + v, u = self.Q_(4.0, "s ** 2"), self.Q_(0.1, "s ** 2") + m = self.ureg.Measurement(v, u) + + for spec, result in ( + ("", "(4.00 +/- 0.10) second ** 2"), + ("P", "(4.00 ± 0.10) second²"), + ("L", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), + ("H", "(4.00 ± 0.10) second2"), + ("C", "(4.00+/-0.10) second**2"), + ("Lx", r"\SI{4.00 +- 0.10}{\second\squared}"), + (".1f", "(4.0 +/- 0.1) second ** 2"), + (".1fP", "(4.0 ± 0.1) second²"), + (".1fL", r"\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}"), + (".1fH", "(4.0 ± 0.1) second2"), + (".1fC", "(4.0+/-0.1) second**2"), + (".1fLx", r"\SI{4.0 +- 0.1}{\second\squared}"), + ): + with subtests.test(spec): + self.ureg.default_format = spec + assert "{}".format(m) == result + def test_raise_build(self): v, u = self.Q_(1.0, "s"), self.Q_(0.1, "s") o = self.Q_(0.1, "m") From 0ec0fde1ac9152d7a3f5aa4c386d6e6022b040be Mon Sep 17 00:00:00 2001 From: Eltos Date: Tue, 1 Feb 2022 17:46:13 +0100 Subject: [PATCH 4/9] Honour default_format fixes https://github.com/hgrecco/pint/issues/1456 --- CHANGES | 1 + pint/measurement.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 1dac418b9..f34a555cc 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ Pint Changelog - Fix casting error when using to_reduced_units with array of int. (Issue #1184) - Use default numpy `np.printoptions` available since numpy 1.15. +- Fix default_format ignored for measurement (Issue #1456) 0.18 (2021-10-26) diff --git a/pint/measurement.py b/pint/measurement.py index 1c5292b9f..01262eb02 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -83,6 +83,9 @@ def __str__(self): return "{}".format(self) def __format__(self, spec): + + spec = spec or self.default_format + # special cases if "Lx" in spec: # the LaTeX siunitx code # the uncertainties module supports formatting From f1dc122abc48eae80d88bc3adeb99081e46e6f29 Mon Sep 17 00:00:00 2001 From: keewis Date: Wed, 2 Feb 2022 22:49:31 +0100 Subject: [PATCH 5/9] implement `numpy.nanprod` (#1369) * add tests for nanprod * fix the exponent for nanprod * use `helpers.assert_quantity_equal` instead of `self.assertQuantityEqual` * add a entry to `CHANGES` --- CHANGES | 1 + pint/numpy_func.py | 65 ++++++++++++++++++++++-------------- pint/testsuite/test_numpy.py | 10 ++++++ 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/CHANGES b/CHANGES index f34a555cc..aa5443de5 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ Pint Changelog - Fix casting error when using to_reduced_units with array of int. (Issue #1184) - Use default numpy `np.printoptions` available since numpy 1.15. +- Implement `numpy.nanprod` (Issue #1369) - Fix default_format ignored for measurement (Issue #1456) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 38aab1ae5..5c48e5a06 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -679,34 +679,49 @@ def _all(a, *args, **kwargs): raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") -@implements("prod", "function") -def _prod(a, *args, **kwargs): - arg_names = ("axis", "dtype", "out", "keepdims", "initial", "where") - all_kwargs = dict(**dict(zip(arg_names, args)), **kwargs) - axis = all_kwargs.get("axis", None) - where = all_kwargs.get("where", None) - - registry = a.units._REGISTRY - - if axis is not None and where is not None: - _, where_ = np.broadcast_arrays(a._magnitude, where) - exponents = np.unique(np.sum(where_, axis=axis)) - if len(exponents) == 1 or (len(exponents) == 2 and 0 in exponents): - units = a.units ** np.max(exponents) +def implement_prod_func(name): + if np is None: + return + + func = getattr(np, name, None) + if func is None: + return + + @implements(name, "function") + def _prod(a, *args, **kwargs): + arg_names = ("axis", "dtype", "out", "keepdims", "initial", "where") + all_kwargs = dict(**dict(zip(arg_names, args)), **kwargs) + axis = all_kwargs.get("axis", None) + where = all_kwargs.get("where", None) + + registry = a.units._REGISTRY + + if axis is not None and where is not None: + _, where_ = np.broadcast_arrays(a._magnitude, where) + exponents = np.unique(np.sum(where_, axis=axis)) + if len(exponents) == 1 or (len(exponents) == 2 and 0 in exponents): + units = a.units ** np.max(exponents) + else: + units = registry.dimensionless + a = a.to(units) + elif axis is not None: + units = a.units ** a.shape[axis] + elif where is not None: + exponent = np.sum(where) + units = a.units ** exponent else: - units = registry.dimensionless - a = a.to(units) - elif axis is not None: - units = a.units ** a.shape[axis] - elif where is not None: - exponent = np.sum(where) - units = a.units ** exponent - else: - units = a.units ** a.size + exponent = ( + np.sum(np.logical_not(np.isnan(a))) if name == "nanprod" else a.size + ) + units = a.units ** exponent + + result = func(a._magnitude, *args, **kwargs) + + return registry.Quantity(result, units) - result = np.prod(a._magnitude, *args, **kwargs) - return registry.Quantity(result, units) +for name in ["prod", "nanprod"]: + implement_prod_func(name) # Implement simple matching-unit or stripped-unit functions based on signature diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index ce337b24b..5e9915bff 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -329,6 +329,16 @@ def test_prod_numpy_func(self): np.prod(self.q, axis=axis, where=[True, False]), [3, 1] * self.ureg.m ** 2 ) + @helpers.requires_array_function_protocol() + def test_nanprod_numpy_func(self): + helpers.assert_quantity_equal(np.nanprod(self.q_nan), 6 * self.ureg.m ** 3) + helpers.assert_quantity_equal( + np.nanprod(self.q_nan, axis=0), [3, 2] * self.ureg.m ** 2 + ) + helpers.assert_quantity_equal( + np.nanprod(self.q_nan, axis=1), [2, 3] * self.ureg.m ** 2 + ) + def test_sum(self): assert self.q.sum() == 10 * self.ureg.m helpers.assert_quantity_equal(self.q.sum(0), [4, 6] * self.ureg.m) From 8532f7760d393adea6f7b31eb0444fcc27b6d5b4 Mon Sep 17 00:00:00 2001 From: David Linke Date: Thu, 3 Feb 2022 22:40:58 +0100 Subject: [PATCH 6/9] Fix use of offset units for higher dimensional units (e.g. gauge pressure #1066) (#1409) * Fix offset-calculation for user-defined offset-units (#1066) * Update of CHANGES --- CHANGES | 2 ++ pint/registry.py | 10 +++++----- pint/testsuite/test_issues.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index aa5443de5..4ef06b7ea 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.19 (unreleased) ----------------- +- Fix a bug for offset units of higher dimension, e.g. gauge pressure. + (Issue #1066, thanks dalito) - Fix type hints of function wrapper (Issue #1431) - Upgrade min version of uncertainties to 3.1.4 - Fix setting options of the application registry (Issue #1403). diff --git a/pint/registry.py b/pint/registry.py index e3b5c7762..77f167d6c 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -71,7 +71,7 @@ from ._typing import F, QuantityOrUnitLike from .compat import HAS_BABEL, babel_parse, tokenizer from .context import Context, ContextChain -from .converters import LogarithmicConverter, ScaleConverter +from .converters import ScaleConverter from .definitions import ( AliasDefinition, Definition, @@ -1463,10 +1463,10 @@ def _validate_and_extract(self, units): return None - def _add_ref_of_log_unit(self, offset_unit, all_units): + def _add_ref_of_log_or_offset_unit(self, offset_unit, all_units): slct_unit = self._units[offset_unit] - if isinstance(slct_unit.converter, LogarithmicConverter): + if slct_unit.is_logarithmic or (not slct_unit.is_multiplicative): # Extract reference unit slct_ref = slct_unit.reference # If reference unit is not dimensionless @@ -1534,13 +1534,13 @@ def _convert(self, value, src, dst, inplace=False): value = self._units[src_offset_unit].converter.to_reference(value, inplace) src = src.remove([src_offset_unit]) # Add reference unit for multiplicative section - src = self._add_ref_of_log_unit(src_offset_unit, src) + src = self._add_ref_of_log_or_offset_unit(src_offset_unit, src) # clean dst units from offset units if dst_offset_unit: dst = dst.remove([dst_offset_unit]) # Add reference unit for multiplicative section - dst = self._add_ref_of_log_unit(dst_offset_unit, dst) + dst = self._add_ref_of_log_or_offset_unit(dst_offset_unit, dst) # Convert non multiplicative units to the dst. value = super()._convert(value, src, dst, inplace, False) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index ed781b72f..55723c3e8 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -717,6 +717,17 @@ def test_issue1062_issue1097(self): q = ureg.Quantity(1, "nm") q.to("J") + def test_issue1066(self): + """Verify calculations for offset units of higher dimension""" + ureg = UnitRegistry() + ureg.define("barga = 1e5 * Pa; offset: 1e5") + ureg.define("bargb = 1 * bar; offset: 1") + q_4barg_a = ureg.Quantity(4, ureg.barga) + q_4barg_b = ureg.Quantity(4, ureg.bargb) + q_5bar = ureg.Quantity(5, ureg.bar) + helpers.assert_quantity_equal(q_4barg_a, q_5bar) + helpers.assert_quantity_equal(q_4barg_b, q_5bar) + def test_issue1086(self): # units with prefixes should correctly test as 'in' the registry assert "bits" in ureg From 2a3b7fdf9ace7760c6ef9d252178f10608600a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Sun, 16 Jan 2022 14:45:38 +0100 Subject: [PATCH 7/9] Drop Support for Python 3.7 - Update CHANGES - Update README - Add black badge in README - Update Docs - Move pull request template to github folder - Update CI with minimal dependencies --- .../pull_request_template.md | 0 .github/workflows/ci.yml | 13 +++++-------- CHANGES | 4 ++++ README.rst | 6 ++++-- docs/getting.rst | 2 +- docs/index.rst | 2 +- pint/__init__.py | 9 +++------ setup.cfg | 11 +++-------- 8 files changed, 21 insertions(+), 26 deletions(-) rename pull_request_template.md => .github/pull_request_template.md (100%) diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35de47f83..5d2ab6a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,21 +7,18 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9] - numpy: [null, "numpy>=1.17,<2.0.0"] - uncertainties: [null, "uncertainties==3.1.4", "uncertainties>=3.1.4,<4.0.0"] + python-version: [3.8, 3.9, "3.10"] + numpy: [null, "numpy>=1.19.5,<2.0.0"] + uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"] extras: [null] include: - - python-version: 3.7 # Minimal versions - numpy: numpy==1.17.5 + - python-version: 3.8 # Minimal versions + numpy: numpy==1.19.5 extras: matplotlib==2.2.5 - python-version: 3.8 numpy: "numpy" uncertainties: "uncertainties" extras: "sparse xarray netCDF4 dask[complete] graphviz babel==2.8" - - python-version: "3.10" - numpy: null - extras: null runs-on: ubuntu-latest env: diff --git a/CHANGES b/CHANGES index 4ef06b7ea..2de0a4ecf 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,10 @@ Pint Changelog - Implement `numpy.nanprod` (Issue #1369) - Fix default_format ignored for measurement (Issue #1456) +### Breaking Changes + +- Change minimal Python version support to 3.8+ +- Change minimal Numpy version support to 1.19+ 0.18 (2021-10-26) ----------------- diff --git a/README.rst b/README.rst index e43930fc9..86c8f77fc 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ :target: https://pypi.python.org/pypi/pint :alt: Latest Version +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/python/black + .. image:: https://readthedocs.org/projects/pint/badge/ :target: https://pint.readthedocs.org/ :alt: Documentation @@ -40,8 +43,7 @@ and constants. Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 3.7+ with no other dependency. -If you need Python 3.6 compatibility, use Pint 0.17. +It has a complete test coverage. It runs in Python 3.8+ with no other dependency. It is licensed under BSD. It is extremely easy and natural to use: diff --git a/docs/getting.rst b/docs/getting.rst index c6f35cce4..0afcea354 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -3,7 +3,7 @@ Installation ============ -Pint has no dependencies except Python_ itself. In runs on Python 3.7+. +Pint has no dependencies except Python_ itself. In runs on Python 3.8+. You can install it (or upgrade to the latest version) using pip_:: diff --git a/docs/index.rst b/docs/index.rst index 108b43e50..806c0ff6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 3.7+ with no other +It has a complete test coverage. It runs in Python 3.8+ with no other dependencies. It is licensed under a `BSD 3-clause style license`_. It is extremely easy and natural to use: diff --git a/pint/__init__.py b/pint/__init__.py index 1ddc7e6e5..5ab92f07f 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -11,6 +11,8 @@ :license: BSD, see LICENSE for more details. """ +from importlib.metadata import version + from .context import Context from .errors import ( # noqa: F401 DefinitionSyntaxError, @@ -29,12 +31,6 @@ from .unit import Unit from .util import logger, pi_theorem # noqa: F401 -try: - from importlib.metadata import version -except ImportError: - # Backport for Python < 3.8 - from importlib_metadata import version - try: # pragma: no cover __version__ = version("pint") except Exception: # pragma: no cover @@ -138,6 +134,7 @@ def test(): "UnitRegistry", "PintError", "DefinitionSyntaxError", + "LogarithmicUnitCalculusError", "DimensionalityError", "OffsetUnitCalculusError", "RedefinitionError", diff --git a/setup.cfg b/setup.cfg index e8a4193ad..939fe0930 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ classifiers = Programming Language :: Python Topic :: Scientific/Engineering Topic :: Software Development :: Libraries - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -27,17 +26,13 @@ classifiers = packages = pint zip_safe = True include_package_data = True -python_requires = >=3.7 -install_requires = - packaging - importlib-metadata; python_version < '3.8' +python_requires = >=3.8 setup_requires = setuptools; setuptools_scm -test_suite = pint.testsuite.testsuite scripts = pint/pint-convert [options.extras_require] -numpy = numpy >= 1.17 -uncertainties = uncertainties >= 3.1.4 +numpy = numpy >= 1.19.5 +uncertainties = uncertainties >= 3.1.6 test = pytest pytest-mpl From d39c6b3d1837d6b8e772d633ae7e5caf19821ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= <43635101+jules-ch@users.noreply.github.com> Date: Wed, 2 Feb 2022 22:50:35 +0100 Subject: [PATCH 8/9] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d2ab6a1f..4f0be8f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: python-version: [3.8, 3.9, "3.10"] - numpy: [null, "numpy>=1.19.5,<2.0.0"] + numpy: [null, "numpy>=1.19,<2.0.0"] uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"] extras: [null] include: From 5403f46ecf636d0749cf54cddf725d177d60af61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Thu, 3 Feb 2022 23:11:40 +0100 Subject: [PATCH 9/9] Fix typing issue on to_reduced_units --- pint/quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index ceaa7df79..1305a5614 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -792,9 +792,9 @@ def to_reduced_units(self) -> Quantity[_MagnitudeType]: # shortcuts in case we're dimensionless or only a single unit if self.dimensionless: - return self.ito({}) + return self.to({}) if len(self._units) == 1: - return None + return self units = self._units.copy() new_units = self._get_reduced_units(units)