Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Pint for openSUSE:Factory checked in at 2026-04-10 17:54:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Pint (Old) and /work/SRC/openSUSE:Factory/.python-Pint.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Pint" Fri Apr 10 17:54:24 2026 rev:26 rq:1345810 version:0.25.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Pint/python-Pint.changes 2026-03-04 21:09:34.895268492 +0100 +++ /work/SRC/openSUSE:Factory/.python-Pint.new.21863/python-Pint.changes 2026-04-10 18:03:46.680503139 +0200 @@ -1,0 +2,18 @@ +Fri Apr 10 11:47:49 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.25.3: + * Replace python-mip with scipy for unit optimization, + fixing crashes on Python 3.12+ (issue #2121). + * Add support for more NumPy functions: vdot, inner, + outer, matvec, vecmat, and tensordot. + * Implement lazy loading for dask to avoid unnecessary + imports. + * Add "^" formatting flag to force non-ratio unit + representation. + * Improve NaN scale handling to prevent incorrect unit + cancellations. + * Add electric_field_gradient to default definitions. + * Ensure string evaluation always returns a Quantity + object. + +------------------------------------------------------------------- Old: ---- pint-0.25.2.tar.gz New: ---- pint-0.25.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Pint.spec ++++++ --- /var/tmp/diff_new_pack.pYTVyg/_old 2026-04-10 18:03:47.848551322 +0200 +++ /var/tmp/diff_new_pack.pYTVyg/_new 2026-04-10 18:03:47.852551487 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-Pint -Version: 0.25.2 +Version: 0.25.3 Release: 0 Summary: Physical quantities module License: BSD-3-Clause ++++++ pint-0.25.2.tar.gz -> pint-0.25.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/PKG-INFO new/pint-0.25.3/PKG-INFO --- old/pint-0.25.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: Pint -Version: 0.25.2 +Version: 0.25.3 Summary: Physical quantities module Project-URL: Homepage, https://github.com/hgrecco/pint Project-URL: Documentation, https://pint.readthedocs.io/ @@ -32,9 +32,9 @@ Requires-Dist: babel<=2.8; extra == 'all' Requires-Dist: dask<2025.3.0; extra == 'all' Requires-Dist: matplotlib; extra == 'all' -Requires-Dist: mip>=1.13; (python_version < '3.13') and extra == 'all' Requires-Dist: numpy>=1.23; extra == 'all' Requires-Dist: pint-pandas>=0.3; extra == 'all' +Requires-Dist: scipy; extra == 'all' Requires-Dist: uncertainties>=3.1.6; extra == 'all' Requires-Dist: xarray; extra == 'all' Provides-Extra: babel @@ -71,12 +71,12 @@ Requires-Dist: sphinx<8.2,>=6; extra == 'docs' Provides-Extra: matplotlib Requires-Dist: matplotlib; extra == 'matplotlib' -Provides-Extra: mip -Requires-Dist: mip>=1.13; (python_version < '3.13') and extra == 'mip' Provides-Extra: numpy Requires-Dist: numpy>=1.23; extra == 'numpy' Provides-Extra: pandas Requires-Dist: pint-pandas>=0.3; extra == 'pandas' +Provides-Extra: scipy +Requires-Dist: scipy; extra == 'scipy' Provides-Extra: test Requires-Dist: pytest; extra == 'test' Requires-Dist: pytest-benchmark; extra == 'test' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/compat.py new/pint-0.25.3/pint/compat.py --- old/pint-0.25.2/pint/compat.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/compat.py 2020-02-02 01:00:00.000000000 +0100 @@ -15,6 +15,7 @@ from collections.abc import Callable, Iterable, Mapping from decimal import Decimal from importlib import import_module +from importlib.util import find_spec from numbers import Number from typing import ( Any, @@ -227,18 +228,13 @@ HAS_NUMPY = False try: - import mip # noqa: F401 + import scipy # noqa: F401 - HAS_MIP = True + HAS_SCIPY = True except ImportError: - HAS_MIP = False + HAS_SCIPY = False -try: - import dask # noqa: F401 - - HAS_DASK = True -except ImportError: - HAS_DASK = False +HAS_DASK = find_spec("dask") is not None ############################## @@ -359,31 +355,23 @@ return value -if HAS_MIP: - import mip - - mip_model = mip.model - mip_Model = mip.Model - mip_INF = mip.INF - mip_INTEGER = mip.INTEGER - mip_xsum = mip.xsum - mip_OptimizationStatus = mip.OptimizationStatus +if HAS_SCIPY: + import scipy else: - mip_missing = missing_dependency("mip") - mip_model = mip_missing - mip_Model = mip_missing - mip_INF = mip_missing - mip_INTEGER = mip_missing - mip_xsum = mip_missing - mip_OptimizationStatus = mip_missing + scipy = missing_dependency("scipy") # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast # types using guarded imports if HAS_DASK: - from dask import array as dask_array from dask.base import compute, persist, visualize + + class _LazyDaskArray: + def __getattr__(self, attr): + return getattr(import_module("dask.array"), attr) + + dask_array = _LazyDaskArray() else: compute, persist, visualize = None, None, None dask_array = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/default_en.txt new/pint-0.25.3/pint/default_en.txt --- old/pint-0.25.2/pint/default_en.txt 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/default_en.txt 2020-02-02 01:00:00.000000000 +0100 @@ -435,6 +435,10 @@ [electric_field] = [electric_potential] / [length] atomic_unit_of_electric_field = e * k_C / a_0 ** 2 = a_u_electric_field +# Electric field gradient +[electric_field_gradient] = [energy] / [area] / [charge] +atomic_unit_of_electric_field_gradient = E_h / a_0 ** 2 / e = a_u_efg + # Electric displacement field [electric_displacement_field] = [charge] / [area] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/_format_helpers.py new/pint-0.25.3/pint/delegates/formatter/_format_helpers.py --- old/pint-0.25.2/pint/delegates/formatter/_format_helpers.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/_format_helpers.py 2020-02-02 01:00:00.000000000 +0100 @@ -167,8 +167,10 @@ Parameters ---------- - items : list - a list of (name, exponent) pairs. + numerator : list + a list of (name, exponent) pairs with positive exponents. + denominator : list + a list of (name, exponent) pairs with negaitve exponents. as_ratio : bool, optional True to display as ratio, False as negative powers. (Default value = True) single_denominator : bool, optional diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/_spec_helpers.py new/pint-0.25.3/pint/delegates/formatter/_spec_helpers.py --- old/pint-0.25.2/pint/delegates/formatter/_spec_helpers.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/_spec_helpers.py 2020-02-02 01:00:00.000000000 +0100 @@ -69,7 +69,7 @@ # sort by length, with longer items first known_flags = sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) - flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")") + flag_re = re.compile("(" + "|".join(known_flags + ["~", "^"]) + ")") custom_flags = flag_re.findall(spec) return "".join(custom_flags) @@ -81,7 +81,10 @@ (i.e those not part of Python's formatting mini language) """ - for flag in sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) + ["~"]: + for flag in sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) + [ + "~", + "^", + ]: if flag: spec = spec.replace(flag, "") return spec diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/_to_register.py new/pint-0.25.3/pint/delegates/formatter/_to_register.py --- old/pint-0.25.2/pint/delegates/formatter/_to_register.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/_to_register.py 2020-02-02 01:00:00.000000000 +0100 @@ -88,11 +88,13 @@ uspec: str = "", **babel_kwds: Unpack[BabelKwds], ) -> str: + if "as_ratio" in babel_kwds.keys(): + babel_kwds.pop("as_ratio") numerator, _denominator = prepare_compount_unit( unit, uspec, **babel_kwds, - as_ratio=False, + as_ratio=False, # required to get _denominator empty registry=self._registry, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/full.py new/pint-0.25.3/pint/delegates/formatter/full.py --- old/pint-0.25.2/pint/delegates/formatter/full.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/full.py 2020-02-02 01:00:00.000000000 +0100 @@ -12,6 +12,7 @@ from __future__ import annotations import locale +import re from collections.abc import Iterable from typing import TYPE_CHECKING, Any, Literal @@ -103,10 +104,9 @@ if k in spec: return v - for k, v in REGISTERED_FORMATTERS.items(): - if k in spec: - orphan_fmt = REGISTERED_FORMATTERS[k] - break + clean_spec = re.sub(r"\~|\^", "", spec) + if clean_spec in REGISTERED_FORMATTERS: + orphan_fmt = REGISTERED_FORMATTERS[clean_spec] else: return self._formatters["D"] @@ -174,6 +174,7 @@ use_plural=use_plural, length=babel_kwds.get("length", None), locale=locale, + as_ratio=False if "^" in spec else True, ) def format_measurement( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/html.py new/pint-0.25.3/pint/delegates/formatter/html.py --- old/pint-0.25.2/pint/delegates/formatter/html.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/html.py 2020-02-02 01:00:00.000000000 +0100 @@ -106,10 +106,13 @@ else: division_fmt = "{}/{}" + as_ratio = babel_kwds.get("as_ratio", True) + assert isinstance(as_ratio, bool) + return formatter( numerator, denominator, - as_ratio=True, + as_ratio=as_ratio, single_denominator=True, product_fmt=r" ", division_fmt=division_fmt, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/latex.py new/pint-0.25.3/pint/delegates/formatter/latex.py --- old/pint-0.25.2/pint/delegates/formatter/latex.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/latex.py 2020-02-02 01:00:00.000000000 +0100 @@ -209,10 +209,13 @@ # division_fmt = r"\frac" + division_fmt.format("[{}]", "[{}]") + as_ratio = babel_kwds.get("as_ratio", True) + assert isinstance(as_ratio, bool) + formatted = formatter( numerator, denominator, - as_ratio=True, + as_ratio=as_ratio, single_denominator=True, product_fmt=r" \cdot ", division_fmt=r"\frac[{}][{}]", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/delegates/formatter/plain.py new/pint-0.25.3/pint/delegates/formatter/plain.py --- old/pint-0.25.2/pint/delegates/formatter/plain.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/delegates/formatter/plain.py 2020-02-02 01:00:00.000000000 +0100 @@ -102,10 +102,13 @@ else: division_fmt = "{} / {}" + as_ratio = babel_kwds.get("as_ratio", True) + assert isinstance(as_ratio, bool) + return formatter( numerator, denominator, - as_ratio=True, + as_ratio=as_ratio, single_denominator=False, product_fmt="{} * {}", division_fmt=division_fmt, @@ -217,10 +220,13 @@ # Division format in compact formatter is not localized. division_fmt = "{}/{}" + as_ratio = babel_kwds.get("as_ratio", True) + assert isinstance(as_ratio, bool) + return formatter( numerator, denominator, - as_ratio=True, + as_ratio=as_ratio, single_denominator=False, product_fmt="*", # TODO: Should this just be ''? division_fmt=division_fmt, @@ -330,10 +336,13 @@ else: division_fmt = "{}/{}" + as_ratio = babel_kwds.get("as_ratio", True) + assert isinstance(as_ratio, bool) + return formatter( numerator, denominator, - as_ratio=True, + as_ratio=as_ratio, single_denominator=False, product_fmt="·", division_fmt=division_fmt, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/facets/numpy/numpy_func.py new/pint-0.25.3/pint/facets/numpy/numpy_func.py --- old/pint-0.25.2/pint/facets/numpy/numpy_func.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/facets/numpy/numpy_func.py 2020-02-02 01:00:00.000000000 +0100 @@ -813,8 +813,18 @@ # If NumPy is not available, do not attempt implement that which does not exist if np is None: return + if "." not in func_str: + func = getattr(np, func_str, None) + else: + parts = func_str.split(".") + module = np + for part in parts[:-1]: + module = getattr(module, part, None) + func = getattr(module, parts[-1], None) - func = getattr(np, func_str) + # if NumPy does not implement it, do not implement it either + if func is None: + return @implements(func_str, "function") def implementation(a, b, **kwargs): @@ -826,7 +836,18 @@ return mag * units -for func_str in ("cross", "dot"): +for func_str in ( + "cross", + "dot", + "vdot", + "inner", + "outer", + "linalg.outer", + "matvec", + "vecmat", + "tensordot", + "linalg.tensordot", +): implement_mul_func(func_str) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/facets/plain/qto.py new/pint-0.25.3/pint/facets/plain/qto.py --- old/pint-0.25.2/pint/facets/plain/qto.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/facets/plain/qto.py 2020-02-02 01:00:00.000000000 +0100 @@ -3,18 +3,10 @@ import bisect import math import numbers -import sys import warnings from typing import TYPE_CHECKING -from ...compat import ( - mip_INF, - mip_INTEGER, - mip_Model, - mip_model, - mip_OptimizationStatus, - mip_xsum, -) +from ...compat import np, scipy from ...errors import UndefinedBehavior from ...util import UnitsContainer, infer_base_unit @@ -209,8 +201,6 @@ ) -> PlainQuantity: """Return Quantity converted to a unit composed of the preferred units. - Note: this feature crashes on Python >= 3.12 (issue #2121). - Examples -------- @@ -222,9 +212,6 @@ <Quantity(4.44822162, 'watt * second')> """ - if sys.version_info.major == 3 and sys.version_info.minor >= 12: - raise Exception("This feature crashes on Python >= 3.12 (issue #2121)") - units = _get_preferred(quantity, preferred_units) return quantity.to(units) @@ -234,8 +221,6 @@ ) -> PlainQuantity: """Return Quantity converted to a unit composed of the preferred units. - Note: this feature crashes on Python >= 3.12 (issue #2121). - Examples -------- @@ -247,9 +232,6 @@ <Quantity(4.44822162, 'watt * second')> """ - if sys.version_info.major == 3 and sys.version_info.minor >= 12: - raise Exception("This feature crashes on Python >= 3.12 (issue #2121)") - units = _get_preferred(quantity, preferred_units) return quantity.ito(units) @@ -374,50 +356,7 @@ # Now that the input data is minimized, setup the optimization problem - # use mip to select units from preferred units - - model = mip_Model() - model.verbose = 0 - - # Make one variable for each candidate unit - - vars = [ - model.add_var(str(unit), lb=-mip_INF, ub=mip_INF, var_type=mip_INTEGER) - for unit in (preferred_units + unpreferred_units) - ] - - # where [u1 ... uN] are powers of N candidate units (vars) - # and [d1(uI) ... dK(uI)] are the K dimensional exponents of candidate unit I - # and [t1 ... tK] are the dimensional exponents of the quantity (quantity) - # create the following constraints - # - # ⎡ d1(u1) ⋯ dK(u1) ⎤ - # [ u1 ⋯ uN ] * ⎢ ⋮ ⋱ ⎢ = [ t1 ⋯ tK ] - # ⎣ d1(uN) dK(uN) ⎦ - # - # in English, the units we choose, and their exponents, when combined, must have the - # target dimensionality - - matrix = [ - [preferred_unit.dimensionality[dimension] for dimension in dimensions] - for preferred_unit in (preferred_units + unpreferred_units) - ] - - # Do the matrix multiplication with mip_model.xsum for performance and create constraints - for i in range(len(dimensions)): - dot = mip_model.xsum([var * vector[i] for var, vector in zip(vars, matrix)]) - # add constraint to the model - model += dot == dimensionality[i] - - # where [c1 ... cN] are costs, 1 when a preferred variable, and a large value when not - # minimize sum(abs(u1) * c1 ... abs(uN) * cN) - - # linearize the optimization variable via a proxy - objective = model.add_var("objective", lb=0, ub=mip_INF, var_type=mip_INTEGER) - - # Constrain the objective to be equal to the sums of the absolute values of the preferred - # unit powers. Do this by making a separate constraint for each permutation of signedness. - # Also apply the cost coefficient, which causes the output to prefer the preferred units + # use scipy.optimize.milp to select units from preferred units # prefer units that interact with fewer dimensions cost = [len(p.dimensionality) for p in preferred_units] @@ -428,37 +367,58 @@ ) # arbitrary, just needs to be larger cost.extend([bias] * len(unpreferred_units)) - for i in range(1 << len(vars)): - sum = mip_xsum( - [ - (-1 if i & 1 << (len(vars) - j - 1) else 1) * cost[j] * var - for j, var in enumerate(vars) - ] - ) - model += objective >= sum + all_units = preferred_units + unpreferred_units + num_units = len(all_units) + num_dims = len(dimensions) + + # where [u_1 ... u_N] are powers of N candidate units (vars) + # introduce auxiliary variables [a_1 ... a_N] to handle absolute values + # such that |u_i| <= a_i + # variables: x = [u_1, ..., u_N, a_1, ..., a_N] + # objective: min c @ x + # where c = [0, ..., 0, cost_1, ..., cost_i] + c = np.concatenate([np.zeros(num_units), np.array(cost)]) + + # u: (-inf, inf), a: (0, inf) + bounds = scipy.optimize.Bounds( + lb=np.concatenate([-np.inf * np.ones(num_units), np.zeros(num_units)]), + ub=np.inf * np.ones(2 * num_units), + ) - model.objective = objective + # constraint: 1 means integer + integrality = np.ones(2 * num_units, dtype=np.intp) + + # constraint: |u_i| <= a_i + E = np.eye(num_units) + constraints = [ + # 1) u_i - a_i <= 0 + scipy.optimize.LinearConstraint(A=np.hstack([E, -E]), lb=-np.inf, ub=0), + # 2) -u_i - a_i <= 0 + scipy.optimize.LinearConstraint(A=np.hstack([-E, -E]), lb=-np.inf, ub=0), + ] + + # constraint: [D, 0] @ [u, a] = dimensionality + if num_dims > 0: + D = np.array( + [[unit.dimensionality[d] for d in dimensions] for unit in all_units] + ).transpose() + + A_eq = np.hstack([D, np.zeros((num_dims, num_units))]) + b_eq = np.array(dimensionality) + constraints.append(scipy.optimize.LinearConstraint(A=A_eq, lb=b_eq, ub=b_eq)) # run the mips minimizer and extract the result if successful - if model.optimize() == mip_OptimizationStatus.OPTIMAL: - optimal_units = [] - min_objective = float("inf") - for i in range(model.num_solutions): - if model.objective_values[i] < min_objective: - min_objective = model.objective_values[i] - optimal_units.clear() - elif model.objective_values[i] > min_objective: - continue - - temp_unit = quantity._REGISTRY.Unit("") - for var in vars: - if var.xi(i): - temp_unit *= quantity._REGISTRY.Unit(var.name) ** var.xi(i) - optimal_units.append(temp_unit) - - sorting_keys = {tuple(sorted(unit._units)): unit for unit in optimal_units} - min_key = sorted(sorting_keys)[0] - result_unit = sorting_keys[min_key] + res = scipy.optimize.milp( + c, constraints=constraints, integrality=integrality, bounds=bounds + ) + + if res.success and res.x is not None: + exponents = np.round(res.x[:num_units]).astype(int) + + result_unit = quantity._REGISTRY.Unit("") + for unit, exponent in zip(all_units, exponents, strict=True): + if exponent != 0: + result_unit *= unit**exponent return result_unit diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/facets/plain/registry.py new/pint-0.25.3/pint/facets/plain/registry.py --- old/pint-0.25.2/pint/facets/plain/registry.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/facets/plain/registry.py 2020-02-02 01:00:00.000000000 +0100 @@ -907,13 +907,6 @@ ) self._get_root_units_recurse(input_units, 1, accumulators, fraction) - if any( - isnan(k) - for k in itertools.chain(fraction["numerator"], fraction["denominator"]) - ): - # If there is a nan factor, the result is nan - return float("nan"), self.UnitsContainer() - # Identify if terms appear in both numerator and denominator def terms_are_unique(fraction): for n_factor, n_exp in fraction["numerator"].items(): @@ -944,11 +937,17 @@ for n_factor, n_exponent in fraction["numerator"].copy().items(): if n_exponent == 0: del fraction["numerator"][n_factor] + elif isinstance(n_factor, tuple): + # Uncancelled NaN factor: result is NaN + factor = float("nan") else: factor *= n_factor**n_exponent for d_factor, d_exponent in fraction["denominator"].copy().items(): if d_exponent == 0: del fraction["denominator"][d_factor] + elif isinstance(d_factor, tuple): + # Uncancelled NaN factor: result is NaN + factor = float("nan") else: factor *= d_factor**-d_exponent @@ -1016,14 +1015,21 @@ if reg.is_base: accumulators[key] += exp2 else: - # Build numerator and denominator + # Build numerator and denominator. + # For NaN scales, use a unit-specific key so that NaN factors + # from different units don't incorrectly cancel each other, + # while NaN factors from the same unit (e.g. truckload appearing + # via kilotruckload) can still cancel correctly. + scale_key = ( + (key, "nan") if isnan(reg.converter.scale) else reg.converter.scale + ) if exp2 < 0: - fraction["denominator"][reg.converter.scale] = ( - fraction["denominator"].get(reg.converter.scale, 0) - exp2 + fraction["denominator"][scale_key] = ( + fraction["denominator"].get(scale_key, 0) - exp2 ) else: - fraction["numerator"][reg.converter.scale] = ( - fraction["numerator"].get(reg.converter.scale, 0) + exp2 + fraction["numerator"][scale_key] = ( + fraction["numerator"].get(scale_key, 0) + exp2 ) if reg.reference is not None: self._get_root_units_recurse( @@ -1472,7 +1478,11 @@ def _define_op(s: str): return self._eval_token(s, case_sensitive=case_sensitive, **values) - return build_eval_tree(gen).evaluate(_define_op) + result = build_eval_tree(gen).evaluate(_define_op) + + if not isinstance(result, self.Quantity): + return self.Quantity(result) + return result # We put this last to avoid overriding UnitsContainer # and I do not want to rename it. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/formatting.py new/pint-0.25.3/pint/formatting.py --- old/pint-0.25.2/pint/formatting.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/formatting.py 2020-02-02 01:00:00.000000000 +0100 @@ -17,7 +17,7 @@ _PRETTY_EXPONENTS, # noqa: F401 ) from .delegates.formatter._format_helpers import ( - join_u as _join, # noqa: F401 + formatter as fh_formatter, # noqa: F401 ) from .delegates.formatter._format_helpers import ( pretty_fmt_exponent as _pretty_fmt_exponent, # noqa: F401 @@ -93,8 +93,6 @@ """ - join_u = _join - if sort is False: items = tuple(items) else: @@ -103,43 +101,20 @@ if not items: return "" - if as_ratio: - fun = lambda x: exp_call(abs(x)) - else: - fun = exp_call - - pos_terms, neg_terms = [], [] - - for key, value in items: - if value == 1: - pos_terms.append(key) - elif value > 0: - pos_terms.append(power_fmt.format(key, fun(value))) - elif value == -1 and as_ratio: - neg_terms.append(key) - else: - neg_terms.append(power_fmt.format(key, fun(value))) - - if not as_ratio: - # Show as Product: positive * negative terms ** -1 - return _join(product_fmt, pos_terms + neg_terms) - - # Show as Ratio: positive terms / negative terms - pos_ret = _join(product_fmt, pos_terms) or "1" - - if not neg_terms: - return pos_ret - - if single_denominator: - neg_ret = join_u(product_fmt, neg_terms) - if len(neg_terms) > 1: - neg_ret = parentheses_fmt.format(neg_ret) - else: - neg_ret = join_u(division_fmt, neg_terms) - - # TODO: first or last pos_ret should be pluralized + numerator = [(key, value) for key, value in items if value >= 0] + denominator = [(key, value) for key, value in items if value < 0] - return _join(division_fmt, [pos_ret, neg_ret]) + return fh_formatter( + numerator=numerator, + denominator=denominator, + as_ratio=as_ratio, + single_denominator=single_denominator, + product_fmt=product_fmt, + division_fmt=division_fmt, + power_fmt=power_fmt, + parentheses_fmt=parentheses_fmt, + exp_call=exp_call, + ) def format_unit(unit, spec: str, registry=None, **options): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/testsuite/helpers.py new/pint-0.25.3/pint/testsuite/helpers.py --- old/pint-0.25.2/pint/testsuite/helpers.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/testsuite/helpers.py 2020-02-02 01:00:00.000000000 +0100 @@ -13,9 +13,9 @@ from ..compat import ( HAS_BABEL, - HAS_MIP, HAS_NUMPY, HAS_NUMPY_ARRAY_FUNCTION, + HAS_SCIPY, HAS_UNCERTAINTIES, NUMPY_VER, ) @@ -157,7 +157,7 @@ requires_not_uncertainties = pytest.mark.skipif( HAS_UNCERTAINTIES, reason="Requires Uncertainties not to be installed." ) -requires_mip = pytest.mark.skipif(not HAS_MIP, reason="Requires MIP") +requires_scipy = pytest.mark.skipif(not HAS_SCIPY, reason="Requires scipy") # Parametrization diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/testsuite/test_formatter.py new/pint-0.25.3/pint/testsuite/test_formatter.py --- old/pint-0.25.2/pint/testsuite/test_formatter.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/testsuite/test_formatter.py 2020-02-02 01:00:00.000000000 +0100 @@ -4,6 +4,7 @@ from pint import formatting as fmt from pint.delegates.formatter._format_helpers import formatter, join_u +from pint.formatting import formatter as pf_formatter class TestFormatter: @@ -55,3 +56,35 @@ assert fmt.format_unit("", "C") == "dimensionless" with pytest.raises(ValueError): fmt.format_unit("m", "W") + + def test_pf_formatter(self): + assert pf_formatter({}.items()) == "" + assert pf_formatter(dict(meter=1).items()) == "meter" + assert pf_formatter(dict(meter=-1).items()) == "1 / meter" + assert pf_formatter(dict(meter=-1).items(), as_ratio=False) == "meter ** -1" + + assert ( + pf_formatter(dict(meter=-1, second=-1).items(), as_ratio=False) + == "meter ** -1 * second ** -1" + ) + assert ( + pf_formatter( + dict(meter=-1, second=-1).items(), + ) + == "1 / meter / second" + ) + assert ( + pf_formatter(dict(meter=-1, second=-1).items(), single_denominator=True) + == "1 / (meter * second)" + ) + assert ( + pf_formatter(dict(meter=-1, second=-2).items()) == "1 / meter / second ** 2" + ) + assert ( + pf_formatter(dict(second=-2, meter=-1).items(), sort=False) + == "1 / second ** 2 / meter" + ) + assert ( + pf_formatter(dict(meter=-1, second=-2).items(), single_denominator=True) + == "1 / (meter * second ** 2)" + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/testsuite/test_issues.py new/pint-0.25.3/pint/testsuite/test_issues.py --- old/pint-0.25.2/pint/testsuite/test_issues.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/testsuite/test_issues.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,8 +2,11 @@ import copy import decimal +import importlib.util import math import pprint +import subprocess +import sys import pytest @@ -1392,3 +1395,83 @@ UnitsContainer({"meter": 1, "centimeter": -1}) ) assert ok_factor == 100.0 + + +def test_issue2261(func_registry): + func_registry.define("truckload = nan kg") + + # Converting to a prefixed form of a NaN unit should give a numeric result + q = func_registry.Quantity("1000 truckloads") + result = q.to("kilotruckload") + assert result.magnitude == 1.0 + + # Reverse direction should also work + q2 = func_registry.Quantity("1 kilotruckload") + result2 = q2.to("truckload") + assert result2.magnitude == 1000.0 + + # NaN should still propagate when converting to a non-NaN unit + q3 = func_registry.Quantity("2 truckloads") + result3 = q3.to("kg") + assert math.isnan(result3.magnitude) + + # Compound units containing a NaN unit should also convert correctly + q4 = func_registry.Quantity("1000 truckload/day") + result4 = q4.to("kilotruckload/day") + assert result4.magnitude == 1.0 + + +def test_issue2265(): + """Check that dask.array is not imported with pint.""" + if importlib.util.find_spec("dask") is None: + pytest.skip("dask is not available") + + command = [ + sys.executable, + "-c", + "import pint, sys; print('dask.array' in sys.modules)", + ] + result = subprocess.run(command, check=True, capture_output=True, text=True) + assert result.stdout.strip() == "False" + + +def test_issue2265_2(): + """Verify that lazy-loaded dask_array.Array works when accessed.""" + if importlib.util.find_spec("dask") is None: + pytest.skip("dask is not available") + + from pint.compat import HAS_DASK, dask_array + + if not HAS_DASK: + pytest.skip("dask is not available") + + # Access dask_array.Array to trigger lazy import + dask_array_class = dask_array.Array + assert dask_array_class is not None + # Verify it's the actual dask Array class by checking module + assert dask_array_class.__module__ == "dask.array.core" + + +def test_issue2256(): + ureg = UnitRegistry() + + from pint import formatting as fmt + from pint.delegates.formatter.plain import PrettyFormatter + + @fmt.register_unit_format("test2256") + def _test_format(unit, registry, **options): + pf = PrettyFormatter(registry) + return pf.format_unit(unit, "~", as_ratio=False) + + q = 2.3e-6 * ureg.m**3 / (ureg.s**2 * ureg.kg) + assert f"{q:test2256}" == "2.3e-06 kg⁻¹·m³·s⁻²" + assert f"{q:~P}" == "2.3×10⁻⁶ m³/kg/s²" + + +def test_issue2256_2(): + ureg = UnitRegistry() + + q = 2.3e-6 * ureg.m**3 / (ureg.s**2 * ureg.kg) + assert f"{q:~P}" == "2.3×10⁻⁶ m³/kg/s²" + assert f"{q:~^P}" == "2.3×10⁻⁶ kg⁻¹·m³·s⁻²" + assert f"{q:^}" == "2.3e-06 kilogram ** -1 * meter ** 3 * second ** -2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/testsuite/test_numpy_func.py new/pint-0.25.3/pint/testsuite/test_numpy_func.py --- old/pint-0.25.2/pint/testsuite/test_numpy_func.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/testsuite/test_numpy_func.py 2020-02-02 01:00:00.000000000 +0100 @@ -290,3 +290,74 @@ z = self.Q_(np.array([1.0, 2.0, 3.0]), "m") with pytest.raises(OffsetUnitCalculusError): np.cross(t, z) + + def test_vdot(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 + # Real case with offset + t = self.Q_(np.array([0.0, 5.0, 10.0]), "degC") + z = self.Q_(np.array([1.0, 2.0, 3.0]), "m") + helpers.assert_quantity_almost_equal( + np.vdot(t, z), self.Q_(1678.9, "kelvin meter") + ) + # Complex case + a = self.Q_(np.array([1j, 2j]), "m") + b = self.Q_(np.array([1j, 2j]), "s") + # conj(1j)*1j + conj(2j)*2j = 1 + 4 = 5 + helpers.assert_quantity_almost_equal(np.vdot(a, b), self.Q_(5.0, "m * s")) + + def test_vdot_no_autoconvert(self): + t = self.Q_(np.array([0.0, 5.0, 10.0]), "degC") + z = self.Q_(np.array([1.0, 2.0, 3.0]), "m") + with pytest.raises(OffsetUnitCalculusError): + np.vdot(t, z) + + def test_inner(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, 5.0, 10.0]), "degC") + z = self.Q_(np.array([1.0, 2.0, 3.0]), "m") + helpers.assert_quantity_almost_equal( + np.inner(t, z), self.Q_(1678.9, "kelvin meter") + ) + + def test_inner_no_autoconvert(self): + t = self.Q_(np.array([0.0, 5.0, 10.0]), "degC") + z = self.Q_(np.array([1.0, 2.0, 3.0]), "m") + with pytest.raises(OffsetUnitCalculusError): + np.inner(t, z) + + def test_outer(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, 5.0]), "degC") + z = self.Q_(np.array([1.0, 2.0]), "m") + # [273.15, 278.15] outer [1.0, 2.0] = [[273.15, 546.3], [278.15, 556.3]] + expected = np.array([[273.15, 546.3], [278.15, 556.3]]) + helpers.assert_quantity_almost_equal( + np.outer(t, z), self.Q_(expected, "kelvin meter") + ) + + def test_outer_no_autoconvert(self): + t = self.Q_(np.array([0.0, 5.0]), "degC") + z = self.Q_(np.array([1.0, 2.0]), "m") + with pytest.raises(OffsetUnitCalculusError): + np.outer(t, z) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pint/testsuite/test_quantity.py new/pint-0.25.3/pint/testsuite/test_quantity.py --- old/pint-0.25.2/pint/testsuite/test_quantity.py 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pint/testsuite/test_quantity.py 2020-02-02 01:00:00.000000000 +0100 @@ -6,7 +6,6 @@ import math import operator as op import pickle -import sys import warnings from unittest.mock import patch @@ -385,11 +384,7 @@ round(abs(self.Q_("2 second").to("millisecond").magnitude - 2000), 7) == 0 ) - @helpers.requires_mip - @pytest.mark.skipif( - sys.version_info.major == 3 and sys.version_info.minor > 11, - reason="Crashes on Python>=3.12 (issue #2121).", - ) + @helpers.requires_scipy def test_to_preferred(self): ureg = self.ureg Q_ = self.Q_ @@ -427,11 +422,7 @@ result = Q_("1 volt").to_preferred(preferred_units) assert result.units == ureg.volts - @helpers.requires_mip - @pytest.mark.skipif( - sys.version_info.major == 3 and sys.version_info.minor > 11, - reason="Crashes on Python>=3.12 (issue #2121).", - ) + @helpers.requires_scipy def test_to_preferred_registry(self): ureg = self.ureg Q_ = self.Q_ @@ -446,11 +437,7 @@ pressure = (Q_(1, "N") * Q_("1 m**-2")).to_preferred() assert pressure.units == ureg.Pa - @helpers.requires_mip - @pytest.mark.skipif( - sys.version_info.major == 3 and sys.version_info.minor > 11, - reason="Crashes on Python>=3.12 (issue #2121).", - ) + @helpers.requires_scipy def test_autoconvert_to_preferred(self): ureg = self.ureg Q_ = self.Q_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pint-0.25.2/pyproject.toml new/pint-0.25.3/pyproject.toml --- old/pint-0.25.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/pint-0.25.3/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -49,11 +49,10 @@ xarray = ["xarray"] # Impose Dask < 2025.3.0, otherwise it causes "RuntimeError: Attempting to use an asynchronous Client in a synchronous context of `dask.compute`" (see Issue #1016 in Dask). dask = ["dask < 2025.3.0"] -# Install of mip crashes from Python 3.13 due to cffi dependency issue (see https://github.com/python-cffi/cffi/issues/48 and https://stackoverflow.com/questions/79463080/building-wheel-for-cffi-fails-when-installing-python-mip) -mip = ["mip >= 1.13; python_version < '3.13'"] +scipy = ["scipy"] matplotlib = ["matplotlib"] all = [ - "pint[numpy,uncertainties,babel,pandas,pandas,xarray,dask,mip,matplotlib]", + "pint[numpy,uncertainties,babel,pandas,pandas,xarray,dask,scipy,matplotlib]", ] docs = [ "sphinx>=6,<8.2", @@ -131,7 +130,7 @@ docs = { features = [ "docs", "numpy", - "mip", + "scipy", "matplotlib", "dask", "xarray", @@ -149,7 +148,7 @@ "pandas", "xarray", "dask", - "mip", + "scipy", "matplotlib", ], solve-group = "default" } @@ -223,5 +222,5 @@ HAS_BABEL = true HAS_UNCERTAINTIES = true HAS_NUMPY = true -HAS_MIP = true +HAS_SCIPY = true HAS_DASK = true
