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

Reply via email to