From 50eb2aff30173eae2b4de4b4b360079e2a28a3ef Mon Sep 17 00:00:00 2001 From: ghostiee-11 <168410465+ghostiee-11@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:53:33 +0530 Subject: [PATCH 1/2] Accept array-like objects in param.Array via __array__ protocol param.Array currently only accepts numpy ndarray instances. With pandas 2.x defaulting to pyarrow-backed string columns, df["col"].values returns an ArrowStringArray which param.Array rejects with a ValueError, even though the object fully supports the numpy array protocol via __array__. This overrides _validate_class_ in Array to also accept objects that implement __array__ or __array_interface__, while still rejecting plain lists, strings, and other non-array types. --- param/parameters.py | 19 ++++++++++- tests/testnumpy.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/param/parameters.py b/param/parameters.py index 2aa22be5..bbb57afb 100644 --- a/param/parameters.py +++ b/param/parameters.py @@ -2230,7 +2230,12 @@ def __init__(self, default=Undefined, **params): class Array(ClassSelector): - """Parameter whose value is a numpy array.""" + """Parameter whose value is a numpy array. + + Accepts numpy ``ndarray`` objects as well as array-like objects that + implement the ``__array__`` protocol (e.g. pandas ``ExtensionArray`` + subclasses such as ``ArrowStringArray``). + """ @typing.overload def __init__( @@ -2246,6 +2251,18 @@ def __init__(self, default=Undefined, **params): from numpy import ndarray super().__init__(default=default, class_=ndarray, **params) + @staticmethod + def _is_array_like(val): + """Return True if *val* supports the numpy array protocol.""" + return hasattr(val, '__array__') or hasattr(val, '__array_interface__') + + def _validate_class_(self, val, class_, is_instance): + # Accept array-like objects (e.g. pandas ExtensionArray, + # ArrowStringArray) that support the numpy array protocol. + if is_instance and not isinstance(val, class_) and self._is_array_like(val): + return + super()._validate_class_(val, class_, is_instance) + @classmethod def serialize(cls, value): if value is None: diff --git a/tests/testnumpy.py b/tests/testnumpy.py index c1ed5e7d..31486afb 100644 --- a/tests/testnumpy.py +++ b/tests/testnumpy.py @@ -78,3 +78,81 @@ class MatParam(param.Parameterized): mp = MatParam() mp.param.pprint() + + def test_array_accepts_array_like_with_dunder_array(self): + """Objects implementing __array__ should be accepted by param.Array.""" + class ArrayLike: + """Minimal array-like with __array__ protocol.""" + def __init__(self, data): + self._data = numpy.asarray(data) + def __array__(self, dtype=None, copy=None): + if dtype is not None: + return self._data.astype(dtype) + return self._data + + class P(param.Parameterized): + arr = param.Array() + + p = P() + array_like = ArrayLike([1, 2, 3]) + p.arr = array_like # Should not raise + numpy.testing.assert_array_equal(numpy.asarray(p.arr), [1, 2, 3]) + + def test_array_accepts_array_like_with_array_interface(self): + """Objects with __array_interface__ should be accepted.""" + class ArrayInterface: + """Minimal object with __array_interface__.""" + def __init__(self, data): + self._arr = numpy.asarray(data) + + @property + def __array_interface__(self): + return self._arr.__array_interface__ + + class P(param.Parameterized): + arr = param.Array() + + p = P() + obj = ArrayInterface([4, 5, 6]) + p.arr = obj # Should not raise + numpy.testing.assert_array_equal(numpy.asarray(p.arr), [4, 5, 6]) + + def test_array_rejects_plain_list(self): + """Plain lists should still be rejected (no __array__ attribute).""" + class P(param.Parameterized): + arr = param.Array() + + p = P() + with self.assertRaises(ValueError): + p.arr = [1, 2, 3] + + def test_array_rejects_string(self): + """Strings should still be rejected.""" + class P(param.Parameterized): + arr = param.Array() + + p = P() + with self.assertRaises(ValueError): + p.arr = "not an array" + + def test_array_accepts_pandas_extension_array(self): + """pandas ExtensionArray subclasses should be accepted.""" + try: + import pandas as pd + except ImportError: + self.skipTest("pandas not available") + + class P(param.Parameterized): + arr = param.Array() + + p = P() + # Categorical array implements __array__ + cat = pd.Categorical(["a", "b", "a"]) + p.arr = cat # Should not raise + + # ArrowStringArray (pandas >= 1.2 with pyarrow) also implements __array__ + try: + arrow_arr = pd.array(["x", "y", "z"], dtype="string[pyarrow]") + p.arr = arrow_arr # Should not raise + except (ImportError, TypeError): + pass # pyarrow not installed, skip this sub-test From f81f0bec59e2b6f0c9346591471e966e87eb2c91 Mon Sep 17 00:00:00 2001 From: ghostiee-11 <168410465+ghostiee-11@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:14:01 +0530 Subject: [PATCH 2/2] Address review: robust _is_array_like, serialize fallback, test coverage - Use getattr with try/except instead of hasattr to handle objects where property access raises non-AttributeError - Verify __array__ is callable, __array_interface__ is not None - serialize: fall back to numpy.asarray(value).tolist() when value lacks .tolist() method - Docstring: mention both __array__ and __array_interface__ protocols - Tests: broaden pyarrow exception catch, add serialize assertions for both the fallback path and pandas ExtensionArray --- param/parameters.py | 18 ++++++++++++++---- tests/testnumpy.py | 8 ++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/param/parameters.py b/param/parameters.py index bbb57afb..b349477f 100644 --- a/param/parameters.py +++ b/param/parameters.py @@ -2233,8 +2233,9 @@ class Array(ClassSelector): """Parameter whose value is a numpy array. Accepts numpy ``ndarray`` objects as well as array-like objects that - implement the ``__array__`` protocol (e.g. pandas ``ExtensionArray`` - subclasses such as ``ArrowStringArray``). + implement the ``__array__`` or ``__array_interface__`` protocols + (e.g. pandas ``ExtensionArray`` subclasses such as + ``ArrowStringArray``). """ @typing.overload @@ -2254,7 +2255,13 @@ def __init__(self, default=Undefined, **params): @staticmethod def _is_array_like(val): """Return True if *val* supports the numpy array protocol.""" - return hasattr(val, '__array__') or hasattr(val, '__array_interface__') + try: + return ( + callable(getattr(val, '__array__', None)) + or getattr(val, '__array_interface__', None) is not None + ) + except Exception: + return False def _validate_class_(self, val, class_, is_instance): # Accept array-like objects (e.g. pandas ExtensionArray, @@ -2267,7 +2274,10 @@ def _validate_class_(self, val, class_, is_instance): def serialize(cls, value): if value is None: return None - return value.tolist() + if hasattr(value, 'tolist'): + return value.tolist() + import numpy + return numpy.asarray(value).tolist() @classmethod def deserialize(cls, value): diff --git a/tests/testnumpy.py b/tests/testnumpy.py index 31486afb..ff54f621 100644 --- a/tests/testnumpy.py +++ b/tests/testnumpy.py @@ -97,6 +97,8 @@ class P(param.Parameterized): array_like = ArrayLike([1, 2, 3]) p.arr = array_like # Should not raise numpy.testing.assert_array_equal(numpy.asarray(p.arr), [1, 2, 3]) + # Verify serialize fallback (ArrayLike has no .tolist()) + self.assertEqual(param.Array.serialize(array_like), [1, 2, 3]) def test_array_accepts_array_like_with_array_interface(self): """Objects with __array_interface__ should be accepted.""" @@ -149,10 +151,12 @@ class P(param.Parameterized): # Categorical array implements __array__ cat = pd.Categorical(["a", "b", "a"]) p.arr = cat # Should not raise + # Verify serialization works on accepted array-like + self.assertEqual(param.Array.serialize(cat), ["a", "b", "a"]) # ArrowStringArray (pandas >= 1.2 with pyarrow) also implements __array__ try: arrow_arr = pd.array(["x", "y", "z"], dtype="string[pyarrow]") p.arr = arrow_arr # Should not raise - except (ImportError, TypeError): - pass # pyarrow not installed, skip this sub-test + except (ImportError, TypeError, ValueError): + pass # pyarrow not installed or dtype unavailable