Repository: hgrecco/pint-pandas

Issue:
`clip` fails on `NA`/`nan` values
The following code fails in the last line with `TypeError: boolean value of NA is ambiguous`:
```python
import numpy as np
import pandas as pd
import pint
import pint_pandas

print(f"{pd.__version__=}")
print(f"{pint.__version__=}")
print(f"{pint_pandas.__version__=}")

u = pint.get_application_registry()

a = np.r_[1,2,np.nan,4,10]
print(f"{a=}")
print(f"{np.clip(a,3,5)=}")

s = pd.Series(data=a)
print(f"{s=}")
print(f"{np.clip(s,3,5)=}")

qs = pd.Series(data=pint_pandas.PintArray.from_1darray_quantity(a*u.m))
print(f"{qs=}")
print(f"{np.clip(qs,3*u.m,5*u.m)=}")
```

It's output before failing in my environment is:
```
pd.__version__='2.2.3'
pint.__version__='0.24.4'
pint_pandas.__version__='0.6.2'
a=array([ 1.,  2., nan,  4., 10.])
np.clip(a,3,5)=array([ 3.,  3., nan,  4.,  5.])
s=0     1.0
1     2.0
2     NaN
3     4.0
4    10.0
dtype: float64
np.clip(s,3,5)=0    3.0
1    3.0
2    NaN
3    4.0
4    5.0
dtype: float64
qs=0     1.0
1     2.0
2     nan
3     4.0
4    10.0
dtype: pint[meter]
```
So, clearly, numpy chose to pass-through nan values in `np.clip`, and so does pandas.

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c52d210..75bfa62 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,23 +1,21 @@
 exclude: '^pint/_vendor'
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0
+    rev: v4.5.0
     hooks:
     -   id: check-yaml
     -   id: end-of-file-fixer
     -   id: trailing-whitespace
--   repo: https://github.com/psf/black
-    rev: 23.7.0
-    hooks:
-    - id: black
-    - id: black-jupyter
--   repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: 'v0.0.282'
+-   repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.1.7
     hooks:
     - id: ruff
-      args: ["--fix"]
+      args: ["--fix", "--show-fixes"]
+      types_or: [ python, pyi, jupyter ]
+    - id: ruff-format
+      types_or: [ python, pyi, jupyter ]
 -   repo: https://github.com/executablebooks/mdformat
-    rev: 0.7.16
+    rev: 0.7.17
     hooks:
     - id: mdformat
       additional_dependencies:
@@ -28,15 +26,3 @@ repos:
     hooks:
       - id: nbstripout
         args: [--extra-keys=metadata.kernelspec metadata.language_info.version]
--   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: "v1.8.0"
-    hooks:
-      - id: mypy
-        verbose: true
-        args: ["--ignore-missing-imports", "--show-error-codes"]
-        additional_dependencies: [
-          "types-requests",
-          "pandas-stubs",
-          "pint",
-          "matplotlib-stubs",
-        ]
diff --git a/CHANGES b/CHANGES
index d1f4a54..037310c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,7 +5,7 @@ pint-pandas Changelog
 0.7 (unreleased)
 --------------------
 
-- Nothing added yet
+- Added `__array_function__` support for numpy fuctions like clip.
 
 
 0.6.2 (2024-07-29)
diff --git a/pint_pandas/pint_array.py b/pint_pandas/pint_array.py
index 93c787e..df5ed65 100644
--- a/pint_pandas/pint_array.py
+++ b/pint_pandas/pint_array.py
@@ -327,6 +327,11 @@ class PintArray(ExtensionArray, ExtensionScalarOpsMixin):
         self.__dict__.update(dct)
         self._Q = self.dtype.ureg.Quantity
 
+    def __array_function__(self, func, types, args, kwargs):
+        args = convert_np_inputs(args)
+        result = func(*args, **kwargs)
+        return self._convert_np_result(result)
+
     def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         out = kwargs.get("out", ())
         for x in inputs + out:
@@ -346,6 +351,8 @@ class PintArray(ExtensionArray, ExtensionScalarOpsMixin):
 
     def _convert_np_result(self, result):
         if isinstance(result, _Quantity) and is_list_like(result.m):
+            if hasattr(result, "ndim") and result.ndim >= 2:
+                raise ValueError("PintArrays may only be 1D, check axis arguement")
             return PintArray.from_1darray_quantity(result)
         elif isinstance(result, _Quantity):
             return result
@@ -356,6 +363,8 @@ class PintArray(ExtensionArray, ExtensionScalarOpsMixin):
             isinstance(item, _Quantity) for item in result
         ):
             return PintArray._from_sequence(result)
+        elif isinstance(result, np.ndarray):
+            return result
         elif result is None:
             # no return value
             return result
@@ -1172,8 +1181,9 @@ class PintSeriesAccessor(object):
     def _validate(obj):
         if not is_pint_type(obj):
             raise AttributeError(
-                "Cannot use 'pint' accessor on objects of "
-                "dtype '{}'.".format(obj.dtype)
+                "Cannot use 'pint' accessor on objects of " "dtype '{}'.".format(
+                    obj.dtype
+                )
             )
 
     @staticmethod
diff --git a/pyproject.toml b/pyproject.toml
index 4b4ab85..c44f88e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -74,6 +74,7 @@ ignore = [
   "E731",
   # line break before binary operator
   # "W503"
+  "F811",
 ]
 extend-exclude = ["build"]
 line-length=88


Test patch:
diff --git a/pint_pandas/testsuite/test_issues.py b/pint_pandas/testsuite/test_issues.py
index 74f26eb..3ea07eb 100644
--- a/pint_pandas/testsuite/test_issues.py
+++ b/pint_pandas/testsuite/test_issues.py
@@ -311,3 +311,16 @@ class TestIssue246(BaseExtensionTests):
 
         # now an operation where each cell is independent from each other
         df.apply(lambda x: x * 2, axis=1)
+
+
+class TestIssue255(BaseExtensionTests):
+    def test_issue255(self):
+        a = np.r_[1, 2, np.nan, 4, 10]
+        pa = PintArray.from_1darray_quantity(a * ureg.m)
+
+        result = np.clip(pa, 3 * ureg.m, 5 * ureg.m)
+
+        e = np.clip(a, 3, 5)
+        expected = PintArray.from_1darray_quantity(e * ureg.m)
+
+        tm.assert_equal(result, expected)
diff --git a/pint_pandas/testsuite/test_pandas_extensiontests.py b/pint_pandas/testsuite/test_pandas_extensiontests.py
index 7d5ba04..163e855 100644
--- a/pint_pandas/testsuite/test_pandas_extensiontests.py
+++ b/pint_pandas/testsuite/test_pandas_extensiontests.py
@@ -603,3 +603,11 @@ class TestPintArray(base.ExtensionTests):
     @pytest.mark.skip("TODO: fix this test")
     def test_array_interface_copy(self, data):
         pass
+
+    @pytest.mark.skip(reason="not implemented in pint")
+    def test_repeat(self):
+        pass
+
+    @pytest.mark.skip(reason="not implemented in pint")
+    def test_repeat_raises(self):
+        pass