Source code for pint.compat

"""
    pint.compat
    ~~~~~~~~~~~

    Compatibility layer.

    :copyright: 2013 by Pint Authors, see AUTHORS for more details.
    :license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

import math
import sys
from collections.abc import Callable, Iterable, Mapping
from decimal import Decimal
from importlib import import_module
from numbers import Number
from typing import (
    Any,
    NoReturn,
    TypeAlias,  # noqa
)

if sys.version_info >= (3, 11):
    from typing import Self  # noqa
else:
    from typing_extensions import Self  # noqa


if sys.version_info >= (3, 11):
    from typing import Never  # noqa
else:
    from typing_extensions import Never  # noqa


if sys.version_info >= (3, 11):
    from typing import Unpack  # noqa
else:
    from typing_extensions import Unpack  # noqa


if sys.version_info >= (3, 13):
    from warnings import deprecated  # noqa
else:
    from typing_extensions import deprecated  # noqa


[docs] def missing_dependency( package: str, display_name: str | None = None ) -> Callable[..., NoReturn]: """Return a helper function that raises an exception when used. It provides a way delay a missing dependency exception until it is used. """ display_name = display_name or package def _inner(*args: Any, **kwargs: Any) -> NoReturn: raise Exception( "This feature requires %s. Please install it by running:\n" "pip install %s" % (display_name, package) ) return _inner
# TODO: remove this warning after v0.10
[docs] class BehaviorChangeWarning(UserWarning): pass
try: from uncertainties import UFloat, ufloat from uncertainties import unumpy as unp HAS_UNCERTAINTIES = True except ImportError: UFloat = ufloat = unp = None HAS_UNCERTAINTIES = False try: import numpy as np from numpy import datetime64 as np_datetime64 from numpy import ndarray HAS_NUMPY = True NUMPY_VER = np.__version__ if HAS_UNCERTAINTIES: NUMERIC_TYPES = (Number, Decimal, ndarray, np.number, UFloat) else: NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): if isinstance(value, (dict, bool)) or value is None: raise TypeError(f"Invalid magnitude for Quantity: {value!r}") elif isinstance(value, str) and value == "": raise ValueError("Quantity magnitude cannot be an empty string.") elif isinstance(value, (list, tuple)): return np.asarray(value) elif HAS_UNCERTAINTIES: from pint.facets.measurement.objects import Measurement if isinstance(value, Measurement): return ufloat(value.value, value.error) if force_ndarray or ( force_ndarray_like and not is_duck_array_type(type(value)) ): return np.asarray(value) return value def _test_array_function_protocol(): # Test if the __array_function__ protocol is enabled try: class FakeArray: def __array_function__(self, *args, **kwargs): return np.concatenate([FakeArray()]) return True except ValueError: return False HAS_NUMPY_ARRAY_FUNCTION = _test_array_function_protocol() NP_NO_VALUE = np._NoValue except ImportError: np = None class ndarray: pass class np_datetime64: pass HAS_NUMPY = False NUMPY_VER = "0" NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False NP_NO_VALUE = None def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): if force_ndarray or force_ndarray_like: raise ValueError( "Cannot force to ndarray or ndarray-like when NumPy is not present." ) elif isinstance(value, (dict, bool)) or value is None: raise TypeError(f"Invalid magnitude for Quantity: {value!r}") elif isinstance(value, str) and value == "": raise ValueError("Quantity magnitude cannot be an empty string.") elif isinstance(value, (list, tuple)): raise TypeError( "lists and tuples are valid magnitudes for " "Quantity only when NumPy is present." ) elif HAS_UNCERTAINTIES: from pint.facets.measurement.objects import Measurement if isinstance(value, Measurement): return ufloat(value.value, value.error) return value try: from babel import Locale from babel import units as babel_units babel_parse = Locale.parse HAS_BABEL = hasattr(babel_units, "format_unit") except ImportError: HAS_BABEL = False babel_parse = missing_dependency("Babel") # noqa: F811 # type:ignore babel_units = babel_parse try: 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 HAS_MIP = True except ImportError: HAS_MIP = False 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 # Defines Logarithm and Exponential for Logarithmic Converter if HAS_NUMPY: from numpy import ( exp, # noqa: F401 log, # noqa: F401 ) else: from math import ( exp, # noqa: F401 log, # noqa: F401 ) # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast # types using guarded imports try: from dask import array as dask_array from dask.base import compute, persist, visualize except ImportError: compute, persist, visualize = None, None, None dask_array = None # TODO: merge with upcast_type_map #: List upcast type names upcast_type_names = ( "pint_pandas.pint_array.PintArray", "xarray.core.dataarray.DataArray", "xarray.core.dataset.Dataset", "xarray.core.variable.Variable", "pandas.core.series.Series", "pandas.core.frame.DataFrame", "xarray.core.dataarray.DataArray", ) #: Map type name to the actual type (for upcast types). upcast_type_map: Mapping[str, type | None] = {k: None for k in upcast_type_names}
[docs] def fully_qualified_name(t: type) -> str: """Return the fully qualified name of a type.""" module = t.__module__ name = t.__qualname__ if module is None or module == "builtins": return name return f"{module}.{name}"
[docs] def check_upcast_type(obj: type) -> bool: """Check if the type object is an upcast type.""" # TODO: merge or unify name with is_upcast_type fqn = fully_qualified_name(obj) if fqn not in upcast_type_map: return False else: module_name, class_name = fqn.rsplit(".", 1) cls = getattr(import_module(module_name), class_name) upcast_type_map[fqn] = cls # This is to check we are importing the same thing. # and avoid weird problems. Maybe instead of return # we should raise an error if false. return obj in upcast_type_map.values()
[docs] def is_upcast_type(other: type) -> bool: """Check if the type object is an upcast type.""" # TODO: merge or unify name with check_upcast_type if other in upcast_type_map.values(): return True return check_upcast_type(other)
[docs] def is_duck_array_type(cls: type) -> bool: """Check if the type object represents a (non-Quantity) duck array type.""" # TODO (NEP 30): replace duck array check with hasattr(other, "__duckarray__") return issubclass(cls, ndarray) or ( not hasattr(cls, "_magnitude") and not hasattr(cls, "_units") and HAS_NUMPY_ARRAY_FUNCTION and hasattr(cls, "__array_function__") and hasattr(cls, "ndim") and hasattr(cls, "dtype") )
[docs] def is_duck_array(obj: type) -> bool: """Check if an object represents a (non-Quantity) duck array type.""" return is_duck_array_type(type(obj))
[docs] def eq(lhs: Any, rhs: Any, check_all: bool) -> bool | Iterable[bool]: """Comparison of scalars and arrays. Parameters ---------- lhs left-hand side rhs right-hand side check_all if True, reduce sequence to single bool; return True if all the elements are equal. Returns ------- bool or array_like of bool """ out = lhs == rhs if check_all and is_duck_array_type(type(out)): return out.all() return out
[docs] def isnan(obj: Any, check_all: bool) -> bool | Iterable[bool]: """Test for NaN or NaT. Parameters ---------- obj scalar or vector check_all if True, reduce sequence to single bool; return True if any of the elements are NaN. Returns ------- bool or array_like of bool. Always return False for non-numeric types. """ if is_duck_array_type(type(obj)): if obj.dtype.kind in "ifc": out = np.isnan(obj) elif obj.dtype.kind in "Mm": out = np.isnat(obj) else: if HAS_UNCERTAINTIES: try: out = unp.isnan(obj) except TypeError: # Not a numeric or UFloat type out = np.full(obj.shape, False) else: # Not a numeric or datetime type out = np.full(obj.shape, False) return out.any() if check_all else out if isinstance(obj, np_datetime64): return np.isnat(obj) elif HAS_UNCERTAINTIES and isinstance(obj, UFloat): return unp.isnan(obj) try: return math.isnan(obj) except TypeError: return False
[docs] def zero_or_nan(obj: Any, check_all: bool) -> bool | Iterable[bool]: """Test if obj is zero, NaN, or NaT. Parameters ---------- obj scalar or vector check_all if True, reduce sequence to single bool; return True if all the elements are zero, NaN, or NaT. Returns ------- bool or array_like of bool. Always return False for non-numeric types. """ out = eq(obj, 0, False) + isnan(obj, False) if check_all and is_duck_array_type(type(out)): return out.all() return out