"""
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 importlib.util import find_spec
from numbers import Number
from typing import (
Any,
# Remove once all dependent packages change their imports.
Never, # noqa
NoReturn,
Self, # noqa
TypeAlias, # noqa
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
[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
# TODO: remove this warning after v0.10
[docs]
class BehaviorChangeWarning(UserWarning):
pass
##############
# try imports
##############
try:
import babel # noqa: F401
from babel import units as babel_units
HAS_BABEL = hasattr(babel_units, "format_unit")
except ImportError:
HAS_BABEL = False
try:
import uncertainties # noqa: F401
HAS_UNCERTAINTIES = True
except ImportError:
HAS_UNCERTAINTIES = False
try:
import numpy # noqa: F401
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
try:
import scipy # noqa: F401
HAS_SCIPY = True
except ImportError:
HAS_SCIPY = False
HAS_DASK = find_spec("dask") is not None
##############################
# Imports are handled here
# in order to be able to have
# them as constants
# in mypy configuration.
##############################
if HAS_BABEL:
from babel import Locale
from babel import units as babel_units
babel_parse = Locale.parse
else:
babel_parse = missing_dependency("Babel") # noqa: F811 # type:ignore
babel_units = babel_parse
Locale = missing_dependency
if HAS_UNCERTAINTIES:
from uncertainties import UFloat, ufloat
unp = None
else:
UFloat = ufloat = unp = None
if HAS_NUMPY:
import numpy as np
from numpy import datetime64 as np_datetime64
from numpy import (
exp, # noqa: F401
log, # noqa: F401
ndarray,
)
NUMPY_VER = np.__version__
if HAS_UNCERTAINTIES:
from uncertainties import unumpy as unp
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
else:
np = None
class ndarray:
pass
class np_datetime64:
pass
from math import (
exp, # noqa: F401
log, # noqa: F401
)
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
if HAS_SCIPY:
import scipy
else:
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.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
# 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",
"pandas.Series",
"pandas.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}