Repository: rapidsai/cudf
Pull request #18311: Add standard data ingestion pipelines to pylibcudf for ndarrays
Author: login: Matt711
Labels: name: feature request
description: New feature or request
name: Python
description: Affects Python cuDF API.
name: breaking
description: Breaking change
name: pylibcudf
description: Issues specific to the pylibcudf package
Created: 2025-03-18T17:30:22Z | Merged: 2025-04-02T23:26:34Z

## Description
<!-- Provide a standalone description of changes in this PR. -->
<!-- Reference any issues closed by this PR with "closes #1234". -->
<!-- Note: The pull request title will be included in the CHANGELOG. -->
Contributes to #15132 and #18214. This PR starts with cupy arrays and adds the skeleton code for array interface objects (eg. numpy arrays). This is breaking because `from_cuda_array_interface_obj` is now `from_cuda_array_interface`.

I think this PR should be sufficient for Curator to replace their existing logic to convert cupy arrays to list columns with `from_pylibcudf` and `from_ndarray`. Eg.
```python
import cudf
import pylibcudf as plc

cudf.Series.from_pylibcudf(
    plc.Column.from_array(...) # call on cupy array
)
```
## Checklist
- [x] I am familiar with the [Contributing Guidelines](https://github.com/rapidsai/cudf/blob/HEAD/CONTRIBUTING.md).
- [x] New or existing tests cover these changes.
- [x] The documentation is up to date with these changes.


Linked issue #1234: [FEA] Create a single RAPIDS library
**Is your feature request related to a problem? Please describe.**
Hi all,

I would like to share with you the following idea:

So just as we have the following imports:

> import pandas as pd
> import tensorflow as tf
> import matplotlib as plt

why do not create a rapids library that could be imported as follows?

> import rapids as rs

or

> from rapids import cudf

I am aware that there are pros about having each library separate, but I think there are also pros about having a single library, such us

- easier for the data scientists to install: conda install rapids
- it creates a kind of identity around rapids (we do not think on cudf or cuml as independent identities, but as a whole).
- we might adopt 'rs' as a shortcut for rapids (currently, I have only found the following use of 'rs' import rhinoscriptsyntax as  rs)

Please, share your thoughts about the above.

Thanks!

P.S.: There are also cons about having a huge library, but I think the pros exceed the cons.
Linked issue comments: user: login: kkraus14
created_at: 2019-07-03T22:56:24Z
body: This is out of scope for now, possibly to be revisited in the future.
author_association: COLLABORATOR

Diff:
diff --git a/python/pylibcudf/pylibcudf/column.pyi b/python/pylibcudf/pylibcudf/column.pyi
index c9f70de3dbf..1a89824e516 100644
--- a/python/pylibcudf/pylibcudf/column.pyi
+++ b/python/pylibcudf/pylibcudf/column.pyi
@@ -1,12 +1,33 @@
 # Copyright (c) 2024, NVIDIA CORPORATION.
 
 from collections.abc import Sequence
-from typing import Any
+from typing import Any, Protocol, TypedDict
 
 from pylibcudf.gpumemoryview import gpumemoryview
 from pylibcudf.scalar import Scalar
 from pylibcudf.types import DataType
 
+class ArrayInterface(TypedDict):
+    shape: tuple[int, ...]
+    typestr: str
+    data: tuple[int, bool]
+    version: int
+    strides: None | tuple[int, ...]
+    descr: None | list[tuple[Any, ...]]
+    mask: None | "SupportsArrayInterface"
+
+class CudaArrayInterface(ArrayInterface):
+    stream: None | int
+    mask: None | "SupportsCudaArrayInterface"
+
+class SupportsCudaArrayInterface(Protocol):
+    @property
+    def __cuda_array_interface__(self) -> CudaArrayInterface: ...
+
+class SupportsArrayInterface(Protocol):
+    @property
+    def __array_interface__(self) -> ArrayInterface: ...
+
 class Column:
     def __init__(
         self,
@@ -35,8 +56,16 @@ class Column:
     def from_scalar(scalar: Scalar, size: int) -> Column: ...
     @staticmethod
     def all_null_like(like: Column, size: int) -> Column: ...
-    @staticmethod
-    def from_cuda_array_interface_obj(obj: Any) -> Column: ...
+    @classmethod
+    def from_cuda_array_interface(
+        cls, obj: SupportsCudaArrayInterface
+    ) -> Column: ...
+    @classmethod
+    def from_array_interface(cls, obj: SupportsArrayInterface) -> Column: ...
+    @classmethod
+    def from_arraylike(
+        cls, obj: SupportsCudaArrayInterface | SupportsArrayInterface
+    ) -> Column: ...
 
 class ListColumnView:
     def __init__(self, column: Column): ...
diff --git a/python/pylibcudf/pylibcudf/column.pyx b/python/pylibcudf/pylibcudf/column.pyx
index 91752f4b25c..7e452d63e8a 100644
--- a/python/pylibcudf/pylibcudf/column.pyx
+++ b/python/pylibcudf/pylibcudf/column.pyx
@@ -1,6 +1,7 @@
 # Copyright (c) 2023-2025, NVIDIA CORPORATION.
 
 from cython.operator cimport dereference
+from libcpp.limits cimport numeric_limits
 from libcpp.memory cimport make_unique, unique_ptr
 from libcpp.utility cimport move
 from rmm.pylibrmm.stream cimport Stream
@@ -12,14 +13,26 @@ from pylibcudf.libcudf.types cimport size_type
 from rmm.pylibrmm.device_buffer cimport DeviceBuffer
 
 from .gpumemoryview cimport gpumemoryview
+from .filling cimport sequence
 from .scalar cimport Scalar
 from .types cimport DataType, size_of, type_id
 from .utils cimport int_to_bitmask_ptr, int_to_void_ptr, _get_stream
 
-import functools
+from functools import cache
+
 
 __all__ = ["Column", "ListColumnView", "is_c_contiguous"]
 
+
+class _Ravelled:
+    def __init__(self, obj):
+        self.obj = obj
+        cai = obj.__cuda_array_interface__.copy()
+        shape = cai["shape"]
+        cai["shape"] = (shape[0]*shape[1],)
+        self.__cuda_array_interface__ = cai
+
+
 cdef class Column:
     """A container of nullable device data as a column of elements.
 
@@ -316,50 +329,150 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If the data is 2D and NumPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        try:
+            iface = obj.__cuda_array_interface__
+        except AttributeError:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            try:
+                import numpy as np
+            except ImportError:
+                raise ImportError(
+                    "NumPy must be installed to use this method on 2D data"
+                )
+            num_rows, num_cols = shape
+            if num_rows < numeric_limits[size_type].max():
+                # TODO: Add a new Scalar.from_py(value, dtype) API so
+                # we can specify the dtype (size_type in this case) too.
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds size_type limit for offsets column."
+                )
+            rav_obj = _Ravelled(obj)
+            data_col = cls(
+                data_type=data_type,
+                size=rav_obj.__cuda_array_interface__["shape"][0],
+                data=gpumemoryview(rav_obj),
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
+            return cls(
+                data_type=DataType(type_id.LIST),
+                size=num_rows,
+                data=None,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[offsets_col, data_col],
+            )
+        else:
+            raise ValueError("Only 1D or 2D arrays are supported")
+
+    @classmethod
+    def from_array(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy
+        or CUDA array interface.
+
+        Parameters
+        ----------
+        obj : object
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input does not implement a supported array interface.
+        ImportError
+            If NumPy is not installed.
+
+        Notes
+        -----
+        - 1D and 2D C-contiguous device arrays are supported.
+          The data are not copied.
+        - For `numpy.ndarray`, this is not yet implemented.
+
+        Examples
+        --------
+        >>> import pylibcudf as plc
+        >>> import cupy as cp
+        >>> cp_arr = cp.array([[1,2],[3,4]])
+        >>> col = plc.Column.from_array(cp_arr)
+        """
+        if hasattr(obj, "__cuda_array_interface__"):
+            return cls.from_cuda_array_interface(obj)
+        if hasattr(obj, "__array_interface__"):
+            return cls.from_array_interface(obj)
+
+        raise TypeError(
+            f"Cannot convert object of type {type(obj)} to a pylibcudf Column"
         )
 
     cpdef DataType type(self):
@@ -448,7 +561,7 @@ cdef class ListColumnView:
         return lists_column_view(self._column.view())
 
 
-@functools.cache
+@cache
 def _datatype_from_dtype_desc(desc):
     mapping = {
         'u1': type_id.UINT8,
diff --git a/python/pylibcudf/pylibcudf/tests/test_column_from_array.py b/python/pylibcudf/pylibcudf/tests/test_column_from_array.py
new file mode 100644
index 00000000000..17f40753311
--- /dev/null
+++ b/python/pylibcudf/pylibcudf/tests/test_column_from_array.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import pyarrow as pa
+import pytest
+from utils import assert_column_eq
+
+import pylibcudf as plc
+
+np = pytest.importorskip("numpy")
+cp = pytest.importorskip("cupy")
+
+NUMPY_DTYPES = [
+    np.int8,
+    np.int16,
+    np.int32,
+    np.int64,
+    np.uint8,
+    np.uint16,
+    np.uint32,
+    np.uint64,
+    np.float32,
+    np.float64,
+    np.bool_,
+]
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_1darray(request):
+    dtype = request.param
+    return np.array([0, 1, 2, 3], dtype=dtype)
+
+
+@pytest.fixture
+def cp_1darray(np_1darray):
+    return cp.asarray(np_1darray)
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_2darray(request):
+    dtype = request.param
+    return np.array([[0, 1, 2], [3, 4, 5]], dtype=dtype)
+
+
+@pytest.fixture
+def cp_2darray(np_2darray):
+    return cp.asarray(np_2darray)
+
+
+def test_from_ndarray_cupy_1d(cp_1darray, np_1darray):
+    expect = pa.array(
+        np_1darray, type=pa.from_numpy_dtype(np_1darray.dtype.type)
+    )
+    got = plc.Column.from_array(cp_1darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_cupy_2d(cp_2darray, np_2darray):
+    dtype = pa.from_numpy_dtype(np_2darray.dtype.type)
+    expect = pa.array(np_2darray.tolist(), type=pa.list_(dtype))
+    got = plc.Column.from_array(cp_2darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_numpy_1d(np_1darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_1darray)
+
+
+def test_from_ndarray_numpy_2d(np_2darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_2darray)
+
+
+def test_non_c_contiguous_raises(cp_2darray):
+    with pytest.raises(
+        ValueError,
+        match="Data must be C-contiguous",
+    ):
+        plc.Column.from_array(cp.asfortranarray(cp_2darray))
+
+
+def test_row_limit_exceed_raises():
+    with pytest.raises(
+        ValueError,
+        match="Number of rows exceeds size_type limit for offsets column.",
+    ):
+        plc.Column.from_array(cp.zeros((2**31, 1)))
+
+
+@pytest.mark.parametrize("obj", [None, "str"])
+def test_from_ndarray_invalid_obj(obj):
+    with pytest.raises(
+        TypeError,
+        match="Cannot convert object of type .* to a pylibcudf Column",
+    ):
+        plc.Column.from_array(obj)
diff --git a/python/pylibcudf/pylibcudf/tests/test_column_from_device.py b/python/pylibcudf/pylibcudf/tests/test_column_from_device.py
index 82a7746fa3b..9de3c41be5f 100644
--- a/python/pylibcudf/pylibcudf/tests/test_column_from_device.py
+++ b/python/pylibcudf/pylibcudf/tests/test_column_from_device.py
@@ -76,6 +76,6 @@ def test_from_cuda_array_interface(
         # patch strides to be None to test C-configuous layout
         monkeypatch.setattr(iface_obj, "strides", None)
 
-    res = plc.Column.from_cuda_array_interface_obj(iface_obj)
+    res = plc.Column.from_cuda_array_interface(iface_obj)
 
     assert_column_eq(input_column, res)


Reviews:
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:32:37Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:21:37Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:22:53Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:25:43Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T19:02:37Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T19:51:19Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:05:24Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:36:41Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:39:05Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T00:12:37Z
user: login: wence-
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:42:13Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:21:12Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:53:47Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:56:29Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T22:09:30Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T15:56:40Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T16:52:56Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:46:46Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:48:41Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:54:51Z
user: login: mroeschke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:57:59Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T19:20:23Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T19:21:26Z
user: login: mroeschke
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T19:34:48Z
user: login: wence-
body: I think we can avoid the need for cupy!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T13:45:43Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:28:39Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:29:07Z
user: login: Matt711
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:34:18Z
user: login: wence-
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:48:02Z
user: login: wence-
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:13:23Z
user: login: wence-
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:14:35Z
user: login: wence-
body: Tiny suggestions for cleanup. Looks good to me now, thanks Matt!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:15:16Z
user: login: vyasr
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:36:10Z

Review comments:
diff_hunk: @@ -15,7 +15,22 @@ from .scalar cimport Scalar
 from .types cimport DataType, size_of, type_id
 from .utils cimport int_to_bitmask_ptr, int_to_void_ptr
 
-import functools
+from functools import cache, singledispatchmethod
+
+
+try:
+    import numpy as np
+    np_error = None
+except ImportError as err:
+    np = None
+    np_error = err
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Note: Tried following approach in #18020 to make imports optional
created_at: 2025-03-18T17:32:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import cupy as cp
+import numpy as np
+import pyarrow as pa
+import pytest
+from utils import assert_column_eq
+
+import pylibcudf as plc
+
+NUMPY_TO_ARROW_DTYPE = {
path: python/pylibcudf/pylibcudf/tests/test_column_from_any.py
user: login: mroeschke
body: `pyarrow.pyarrow.from_numpy_dtype` should be able to do this for you
created_at: 2025-03-18T18:21:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,8 +35,10 @@ class Column:
     def from_scalar(scalar: Scalar, size: int) -> Column: ...
     @staticmethod
     def all_null_like(like: Column, size: int) -> Column: ...
-    @staticmethod
+    @classmethod
     def from_cuda_array_interface_obj(obj: Any) -> Column: ...
path: python/pylibcudf/pylibcudf/column.pyi
user: login: mroeschke
body: ```suggestion
    def from_cuda_array_interface_obj(cls, obj: Any) -> Column: ...
```
created_at: 2025-03-18T18:22:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -321,6 +338,80 @@ cdef class Column:
             []
         )
 
+    @singledispatchmethod
+    @classmethod
+    def from_any(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: Suggestion: Maybe we can start with an `from_ndarray` method that accepts `numpy` and `cupy` arrays, and then build up an `from_any` when we build up more ingestion methods (e.g. `from_pyobject`, `from_pandas`, etc)
created_at: 2025-03-18T18:25:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -321,6 +338,80 @@ cdef class Column:
             []
         )
 
+    @singledispatchmethod
+    @classmethod
+    def from_any(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Would we eventually remove `from_ndarray` in favor of`from_any`? If so, I'd rather keep `from_any` since its ultimately what we want. And because we're planning on adding more ingestion methods during 25.06 release.

If we plan to keep it, then I'll change `from_any` to `from_ndarray`. I wanted to add a stable API sooner rather than later for Curator's use case. They need to be able create a list column from a cupy array. It should be public and stable so they can keep doing something like the following without it breaking between cudf releases.
```python
cudf.Series.from_pylibcudf(
    plc.Column.from_any(
        cupy_array
    )
)
```

*Also forgot to add a doc string, will add that too.
created_at: 2025-03-18T19:02:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import cupy as cp
+import numpy as np
+import pyarrow as pa
+import pytest
+from utils import assert_column_eq
+
+import pylibcudf as plc
+
+NUMPY_TO_ARROW_DTYPE = {
path: python/pylibcudf/pylibcudf/tests/test_column_from_any.py
user: login: Matt711
body: Thanks
created_at: 2025-03-18T19:51:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -321,6 +338,80 @@ cdef class Column:
             []
         )
 
+    @singledispatchmethod
+    @classmethod
+    def from_any(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: Ah OK I do see now that @vyasr proposed in #15132 that a `from_any` method would just dispatch to another public method e.g. `from_ndarray` (for example), by `singledispatch`. So we would have both a public `from_any` and `from_ndarray` on the column method in this PR.
created_at: 2025-03-18T20:05:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,8 +35,10 @@ class Column:
     def from_scalar(scalar: Scalar, size: int) -> Column: ...
     @staticmethod
     def all_null_like(like: Column, size: int) -> Column: ...
-    @staticmethod
-    def from_cuda_array_interface_obj(obj: Any) -> Column: ...
+    @classmethod
+    def from_cuda_array_interface_obj(cls, obj: Any) -> Column: ...
+    @classmethod
+    def from_any(cls, obj: Any) -> Column: ...
path: python/pylibcudf/pylibcudf/column.pyi
user: login: Matt711
body: ```suggestion
    def from_ndarray(cls, obj: Any) -> Column: ...
```
created_at: 2025-03-18T20:36:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -321,6 +338,80 @@ cdef class Column:
             []
         )
 
+    @singledispatchmethod
+    @classmethod
+    def from_any(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Yes, but I'll start with `from_ndarray` in this PR and add `from_any` in a follow-up PR.
created_at: 2025-03-18T20:39:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,82 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import cupy as cp
+import numpy as np
path: python/pylibcudf/pylibcudf/tests/test_column_from_ndarray.py
user: login: mroeschke
body: Could you use define a fixture that does `pytest.importorskip("numpy"/"cupy")` (like in https://github.com/rapidsai/cudf/pull/18020/files#diff-4ca9193a8f5aa8079576b6ba20cffb2f875bf62fe8f75921f6f151250915947aR11) since technically these are optional dependencies?
created_at: 2025-03-21T00:12:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,8 +35,10 @@ class Column:
     def from_scalar(scalar: Scalar, size: int) -> Column: ...
     @staticmethod
     def all_null_like(like: Column, size: int) -> Column: ...
-    @staticmethod
-    def from_cuda_array_interface_obj(obj: Any) -> Column: ...
+    @classmethod
+    def from_cuda_array_interface_obj(cls, obj: Any) -> Column: ...
+    @classmethod
+    def from_ndarray(cls, obj: Any) -> Column: ...
path: python/pylibcudf/pylibcudf/column.pyi
user: login: wence-
body: Let's tighten this typing. We can define a `Protocol`:

```
from typing import TypedDict, Any, Protocol, Union


class CAI(TypedDict):
    shape: tuple[int, ...]
    typestr: str
    data: tuple[int, bool]
    version: int
    strides: None | tuple[int, ...]
    # TODO: better type for this
    descr: None | tuple[Any, ...]
    mask: Union[None, "SupportsCAI"]
    stream: None | int


class SupportsCAI(Protocol):
    @property
    def __cuda_array_interface__(self) -> CAI: ...

@classmethod
def from_cuda_array_interface_obj(cls, obj: SupportsCAI) -> Column: ...
```

Similarly we can probably do the same for the array interface.
created_at: 2025-03-21T13:06:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,8 +329,8 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
+    @classmethod
+    def from_cuda_array_interface_obj(cls, object obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: How about we call this `from_cuda_array_interface`
created_at: 2025-03-21T13:06:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: I think we don't need singledispatch, and should just handle anything that supports the array interface.
created_at: 2025-03-21T14:19:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
+
+    if cp is not None:
+        @classmethod
+        def _from_2d_cupy_array(cls, object arr):
+            """Convert a 2D CuPy array to a Column."""
+            flat_data = arr.ravel()
+
+            num_rows, num_cols = arr.shape
+            offsets = cp.arange(0, (num_rows + 1) * num_cols, num_cols, dtype=cp.int32)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: *nit*: use `size_type`.
created_at: 2025-03-21T14:20:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,22 @@ from .scalar cimport Scalar
 from .types cimport DataType, size_of, type_id
 from .utils cimport int_to_bitmask_ptr, int_to_void_ptr
 
-import functools
+from functools import cache, singledispatchmethod
+
+
+try:
+    import numpy as np
+    np_error = None
+except ImportError as err:
+    np = None
+    np_error = err
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: This captures, in `err` (if we have an import error) any live variables for the lifetime of the process. If that import happens to be done not at top-level, that might be a lot of stuff.

I would prefer not saving the error and just if `np`/`cp` is `None` raising when we come to use things.

(We should probably do the same in the scalar handling).
created_at: 2025-03-21T14:23:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: I think we should have two methods:

- `from_array_interface`
- `from_cuda_array_interface`

Or maybe `from_arraylike` where the object supports either the cuda array interface or the array interface?
created_at: 2025-03-21T14:24:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: This raises too eagerly in the case where we're trying to convert from a numpy array and the user has numpy but not cupy.
created_at: 2025-03-21T14:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
+
+    if cp is not None:
+        @classmethod
+        def _from_2d_cupy_array(cls, object arr):
+            """Convert a 2D CuPy array to a Column."""
+            flat_data = arr.ravel()
+
+            num_rows, num_cols = arr.shape
+            offsets = cp.arange(0, (num_rows + 1) * num_cols, num_cols, dtype=cp.int32)
+
+            data_view = gpumemoryview(flat_data)
+            offsets_view = gpumemoryview(offsets)
+            typestr = arr.__cuda_array_interface__['typestr'][1:]
+
+            data_col = cls(
+                data_type=_datatype_from_dtype_desc(typestr),
+                size=flat_data.size,
+                data=data_view,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: I think if the data are C-contiguous we can do this without a copy.

In that case, we can also avoid requiring cupy, because we can make the offsets column with `pylibcudf.filling.sequence`.
created_at: 2025-03-21T14:40:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
+
+    if cp is not None:
+        @classmethod
+        def _from_2d_cupy_array(cls, object arr):
+            """Convert a 2D CuPy array to a Column."""
+            flat_data = arr.ravel()
+
+            num_rows, num_cols = arr.shape
+            offsets = cp.arange(0, (num_rows + 1) * num_cols, num_cols, dtype=cp.int32)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: Someone needs to check that this will not produce a column with more than the maximum number of rows. The way we represent list columns in libcudf is that we have a column of `N` rows. But the offsets column has `N+1` rows. But if `N == size_type::max()` then `N+1` overflows, so we can't represent the offsets.
created_at: 2025-03-21T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,22 @@ from .scalar cimport Scalar
 from .types cimport DataType, size_of, type_id
 from .utils cimport int_to_bitmask_ptr, int_to_void_ptr
 
-import functools
+from functools import cache, singledispatchmethod
+
+
+try:
+    import numpy as np
+    np_error = None
+except ImportError as err:
+    np = None
+    np_error = err
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: As a compromise we could save the exception class and string error message but not the traceback frames so we have a more faithful representation of the original error, but I don't have a strong opinion how we reduce this 
created_at: 2025-03-21T16:21:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
+
+    if cp is not None:
+        @classmethod
+        def _from_2d_cupy_array(cls, object arr):
+            """Convert a 2D CuPy array to a Column."""
+            flat_data = arr.ravel()
+
+            num_rows, num_cols = arr.shape
+            offsets = cp.arange(0, (num_rows + 1) * num_cols, num_cols, dtype=cp.int32)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: What do you mean here?
created_at: 2025-03-31T20:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +377,110 @@ cdef class Column:
             []
         )
 
+    @classmethod
+    def from_ndarray(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy array interface.
+
+        Parameters
+        ----------
+        obj : Any
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input type is neither `numpy.ndarray` nor `cupy.ndarray`.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        If `obj` is a 2D CuPy array, the resulting column is a list column.
+        NumPy conversion logic is not yet implemented.
+        Multi-dimensional arrays (ndim > 2) are not supported.
+        """
+        return cls._from_ndarray(obj)
+
+    @singledispatchmethod
+    @classmethod
+    def _from_ndarray(cls, obj):
+        if np_error is not None:
+            raise np_error
+        if cp_error is not None:
+            raise cp_error
+        raise TypeError(f"Cannot convert a {type(obj)} to a pylibcudf Column")
+
+    if np is not None:
+        @classmethod
+        def from_numpy_array(cls, object obj):
+            # TODO: Should expand to support __array_interface__
+            raise NotImplementedError(
+                "Converting to a pylibcudf Column from "
+                "a numpy object is not yet implemented."
+            )
+
+        @_from_ndarray.register(np.ndarray)
+        @classmethod
+        def _(cls, obj):
+            return cls.from_numpy_array(obj)
+
+    if cp is not None:
+        @classmethod
+        def _from_2d_cupy_array(cls, object arr):
+            """Convert a 2D CuPy array to a Column."""
+            flat_data = arr.ravel()
+
+            num_rows, num_cols = arr.shape
+            offsets = cp.arange(0, (num_rows + 1) * num_cols, num_cols, dtype=cp.int32)
+
+            data_view = gpumemoryview(flat_data)
+            offsets_view = gpumemoryview(offsets)
+            typestr = arr.__cuda_array_interface__['typestr'][1:]
+
+            data_col = cls(
+                data_type=_datatype_from_dtype_desc(typestr),
+                size=flat_data.size,
+                data=data_view,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Data's already required to be C-contiguous. I switched to use `plc.filling.sequence` in https://github.com/rapidsai/cudf/pull/18311/commits/3e5dafac1cdecc4c8442d96f253dcc566a684e8f
created_at: 2025-03-31T20:56:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,138 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting a pylibcudf Column is not yet implemented."
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: ```suggestion
            "Converting to a pylibcudf Column is not yet implemented."
```
created_at: 2025-03-31T22:09:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,144 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is required but not installed.
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: ```suggestion
            If CuPy is not installed.
```
created_at: 2025-04-01T15:56:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,144 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
+            raise ImportError("NumPy and CuPy must be installed to use this method")
 
-        typestr = iface['typestr'][1:]
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
+
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            arr = obj
+            flat_data = cp.ravel(arr)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: ```suggestion
            flat_data = cp.ravel(obj)
```
created_at: 2025-04-01T16:52:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
+            raise ImportError("NumPy and CuPy must be installed to use this method")
 
-        typestr = iface['typestr'][1:]
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
+
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            flat_data = cp.ravel(obj)
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds int32 limit for offsets column."
+                )
+            data_col = cls(
+                data_type=data_type,
+                size=flat_data.size,
+                data=gpumemoryview(flat_data),
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
+            return cls(
+                data_type=DataType(type_id.LIST),
+                size=num_rows,
+                data=None,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[offsets_col, data_col],
+            )
+        else:
+            raise ValueError("Only 1D or 2D arrays are supported")
+
+    @classmethod
+    def from_arraylike(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy
+        or CUDA array interface.
+
+        Parameters
+        ----------
+        obj : object
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input does not implement a supported array interface.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        - For `cupy.ndarray`, this uses the CUDA array interface and
+          supports 1D or 2D arrays.
+        - For `numpy.ndarray`, this is not yet implemented.
+
+        Examples:
+            >>> import pylibcudf as plc
+            >>> import cupy as cp
+            >>> cp_arr = cp.array([[1,2],[3,4]])
+            >>> col = plc.Column.from_arraylike(cp_arr)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: ```suggestion
        Examples
        --------
        >>> import pylibcudf as plc
        >>> import cupy as cp
        >>> cp_arr = cp.array([[1,2],[3,4]])
        >>> col = plc.Column.from_arraylike(cp_arr)
```
created_at: 2025-04-01T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
+            raise ImportError("NumPy and CuPy must be installed to use this method")
 
-        typestr = iface['typestr'][1:]
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
+
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            flat_data = cp.ravel(obj)
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds int32 limit for offsets column."
+                )
+            data_col = cls(
+                data_type=data_type,
+                size=flat_data.size,
+                data=gpumemoryview(flat_data),
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
+            return cls(
+                data_type=DataType(type_id.LIST),
+                size=num_rows,
+                data=None,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[offsets_col, data_col],
+            )
+        else:
+            raise ValueError("Only 1D or 2D arrays are supported")
+
+    @classmethod
+    def from_arraylike(cls, obj):
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: Curious, are you planning on supporting an "arraylike" like a Python `list` with this API as well?

If you're planning on constraining this to interface objects, I may suggest just `from_array`
created_at: 2025-04-01T18:48:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: Technically we only need this in the `elif len(shape) == 2:` case?
created_at: 2025-04-01T18:54:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
+            raise ImportError("NumPy and CuPy must be installed to use this method")
 
-        typestr = iface['typestr'][1:]
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
+
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            flat_data = cp.ravel(obj)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: mroeschke
body: We may need to call `cupy.asarray(obj).ravel()` as `obj` could be an arbitrary object that has `__cuda_array_interface__`
created_at: 2025-04-01T18:57:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Yes, thanks
created_at: 2025-04-01T19:20:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,143 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
-            A Column containing the data from the CUDA array interface.
-
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+            A 1D or 2D list column, depending on the input shape.
+
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        if np is None or cp is None:
+            raise ImportError("NumPy and CuPy must be installed to use this method")
 
-        typestr = iface['typestr'][1:]
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
+
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            flat_data = cp.ravel(obj)
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Thanks, done in https://github.com/rapidsai/cudf/pull/18311/commits/849357fcf8686a7b2dc2c0f047357a154f185530
created_at: 2025-04-01T19:21:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: ```suggestion
            Must implement the ``__cuda_array_interface__`` protocol.
```
created_at: 2025-04-02T13:09:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds int32 limit for offsets column."
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: ```suggestion
                    "Number of rows exceeds size_type limit for offsets column."
```
created_at: 2025-04-02T13:12:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,7 @@
 # Copyright (c) 2023-2025, NVIDIA CORPORATION.
 
 from cython.operator cimport dereference
+from libc.limits cimport INT_MAX
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: ```suggestion
from libcpp.limits cimport numeric_limits
```
created_at: 2025-04-02T13:14:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: ```suggestion
            if num_rows < numeric_limits[size_type].max():
```
created_at: 2025-04-02T13:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: *nit*: This should be `size_type`, but we shouldn't introduce the mapping.

What I would like (followup) is a `Scalar.from_py(value, dtype)` so you can specify the dtype when coming from python.
created_at: 2025-04-02T13:17:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: Since we've already validated that the data are c-contiguous we don't need this ravel.
created_at: 2025-04-02T13:19:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,11 +12,24 @@ from pylibcudf.libcudf.types cimport size_type
 from rmm.pylibrmm.device_buffer cimport DeviceBuffer
 
 from .gpumemoryview cimport gpumemoryview
+from .filling cimport sequence
 from .scalar cimport Scalar
 from .types cimport DataType, size_of, type_id
 from .utils cimport int_to_bitmask_ptr, int_to_void_ptr
 
-import functools
+from functools import cache
+
+
+try:
+    import numpy as np
+except ImportError:
+    np = None
+
+try:
+    import cupy as cp
+except ImportError:
+    cp = None
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: I would prefer for these to be lazily imported so that if we don't need this functionality we don't pay the import cost (importing cupy costs about 3 times as long as importing all of pylibcudf right now).
created_at: 2025-04-02T13:36:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds int32 limit for offsets column."
+                )
+            data_col = cls(
+                data_type=data_type,
+                size=flat_data.size,
+                data=gpumemoryview(flat_data),
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: I think we can avoid the need for cupy entirely with this:

```
class Ravelled:
    def __init__(self, obj: SupportsCAI):
        self.obj = obj
        cai = obj.__cuda_array_interface__.copy()
        shape = cai["shape"]
        cai["shape"] = (functools.reduce(operator.mul, shape),)
        self.__cuda_array_interface__ = cai

     data_col = cls(..., data=gpumemoryview(Ravelled(obj)), ...)
```
created_at: 2025-04-02T13:42:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import pyarrow as pa
+import pytest
+from utils import assert_column_eq
+
+import pylibcudf as plc
+
+np = pytest.importorskip("numpy")
+cp = pytest.importorskip("cupy")
+
+NUMPY_DTYPES = [
+    np.int8,
+    np.int16,
+    np.int32,
+    np.int64,
+    np.uint8,
+    np.uint16,
+    np.uint32,
+    np.uint64,
+    np.float32,
+    np.float64,
+    np.bool_,
+]
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_1darray(request):
+    dtype = request.param
+    return np.array([0, 1, 2, 3], dtype=dtype)
+
+
+@pytest.fixture
+def cp_1darray(np_1darray):
+    return cp.asarray(np_1darray)
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_2darray(request):
+    dtype = request.param
+    return np.array([[0, 1, 2], [3, 4, 5]], dtype=dtype)
+
+
+@pytest.fixture
+def cp_2darray(np_2darray):
+    return cp.asarray(np_2darray)
+
+
+def test_from_ndarray_cupy_1d(cp_1darray, np_1darray):
+    expect = pa.array(
+        np_1darray, type=pa.from_numpy_dtype(np_1darray.dtype.type)
+    )
+    got = plc.Column.from_array(cp_1darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_cupy_2d(cp_2darray, np_2darray):
+    dtype = pa.from_numpy_dtype(np_2darray.dtype.type)
+    expect = pa.array(np_2darray.tolist(), type=pa.list_(dtype))
+    got = plc.Column.from_array(cp_2darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_numpy_1d(np_1darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_1darray)
+
+
+def test_from_ndarray_numpy_2d(np_2darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_2darray)
+
+
path: python/pylibcudf/pylibcudf/tests/test_column_from_array.py
user: login: wence-
body: Can we add tests validating that we get errors if trying to convert from data that is not c contiguous (if those do not already exist)?
created_at: 2025-04-02T13:44:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,12 +1,33 @@
 # Copyright (c) 2024, NVIDIA CORPORATION.
 
 from collections.abc import Sequence
-from typing import Any
+from typing import Any, Protocol, TypedDict
 
 from pylibcudf.gpumemoryview import gpumemoryview
 from pylibcudf.scalar import Scalar
 from pylibcudf.types import DataType
 
+class AI(TypedDict):
+    shape: tuple[int, ...]
+    typestr: str
+    data: tuple[int, bool]
+    version: int
+    strides: None | tuple[int, ...]
+    descr: None | list[tuple[Any, ...]]
+    mask: None | "SupportsAI"
+
+class CAI(AI):
+    stream: None | int
+    mask: None | "SupportsCAI"
+
+class SupportsCAI(Protocol):
path: python/pylibcudf/pylibcudf/column.pyi
user: login: wence-
body: *nit*: Let's spell out as `ArrayInterface` and `CudaArrayInterface`
created_at: 2025-04-02T13:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Thanks!
created_at: 2025-04-02T14:28:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +328,145 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__cuda_array_interface__` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            if np is None or cp is None:
+                raise ImportError(
+                    "NumPy and CuPy must be installed to use this method on 2D data"
+                )
+            flat_data = cp.asarray(obj).ravel()
+            num_rows, num_cols = shape
+            if num_rows < INT_MAX:
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
path: python/pylibcudf/pylibcudf/column.pyx
user: login: Matt711
body: Will do
created_at: 2025-04-02T14:29:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+# Copyright (c) 2025, NVIDIA CORPORATION.
+
+import pyarrow as pa
+import pytest
+from utils import assert_column_eq
+
+import pylibcudf as plc
+
+np = pytest.importorskip("numpy")
+cp = pytest.importorskip("cupy")
+
+NUMPY_DTYPES = [
+    np.int8,
+    np.int16,
+    np.int32,
+    np.int64,
+    np.uint8,
+    np.uint16,
+    np.uint32,
+    np.uint64,
+    np.float32,
+    np.float64,
+    np.bool_,
+]
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_1darray(request):
+    dtype = request.param
+    return np.array([0, 1, 2, 3], dtype=dtype)
+
+
+@pytest.fixture
+def cp_1darray(np_1darray):
+    return cp.asarray(np_1darray)
+
+
+@pytest.fixture(params=NUMPY_DTYPES, ids=repr)
+def np_2darray(request):
+    dtype = request.param
+    return np.array([[0, 1, 2], [3, 4, 5]], dtype=dtype)
+
+
+@pytest.fixture
+def cp_2darray(np_2darray):
+    return cp.asarray(np_2darray)
+
+
+def test_from_ndarray_cupy_1d(cp_1darray, np_1darray):
+    expect = pa.array(
+        np_1darray, type=pa.from_numpy_dtype(np_1darray.dtype.type)
+    )
+    got = plc.Column.from_array(cp_1darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_cupy_2d(cp_2darray, np_2darray):
+    dtype = pa.from_numpy_dtype(np_2darray.dtype.type)
+    expect = pa.array(np_2darray.tolist(), type=pa.list_(dtype))
+    got = plc.Column.from_array(cp_2darray)
+    assert_column_eq(expect, got)
+
+
+def test_from_ndarray_numpy_1d(np_1darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_1darray)
+
+
+def test_from_ndarray_numpy_2d(np_2darray):
+    with pytest.raises(
+        NotImplementedError,
+        match="Converting to a pylibcudf Column is not yet implemented.",
+    ):
+        plc.Column.from_array(np_2darray)
+
+
path: python/pylibcudf/pylibcudf/tests/test_column_from_array.py
user: login: Matt711
body: done https://github.com/rapidsai/cudf/pull/18311/commits/617efbda436d7529708aee2039ed7c7cd8825599#diff-d81ea7feeafcf39a47f29ed628804e45bcab30c98d83f8df7be36d2757fa3957
created_at: 2025-04-02T14:34:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +317,148 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            try:
+                import numpy as np
+            except ImportError:
+                raise ImportError(
+                    "NumPy must be installed to use this method on 2D data"
+                )
+            num_rows, num_cols = shape
+            if num_rows < numeric_limits[size_type].max():
+                # TODO: Add a new Scalar.from_py(value, dtype) API so
+                # we can specify the dtype (size_type in this case) too.
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds size_type limit for offsets column."
+                )
+            data_col = cls(
+                data_type=data_type,
+                size=shape[0]*shape[1],
+                data=gpumemoryview(obj),
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: This doesn't quite work because the `__len__` of the `gpumemoryview` object is incorrect. Hence my suggestion for a trivial `Ravel` wrapper around c-contiguous 2D arrays.
created_at: 2025-04-02T14:48:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +317,157 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            try:
+                import numpy as np
+            except ImportError:
+                raise ImportError(
+                    "NumPy must be installed to use this method on 2D data"
+                )
+            num_rows, num_cols = shape
+            if num_rows < numeric_limits[size_type].max():
+                # TODO: Add a new Scalar.from_py(value, dtype) API so
+                # we can specify the dtype (size_type in this case) too.
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds size_type limit for offsets column."
+                )
+
+            class _Ravelled:
+                def __init__(self, obj):
+                    self.obj = obj
+                    cai = obj.__cuda_array_interface__.copy()
+                    shape = cai["shape"]
+                    cai["shape"] = (shape[0]*shape[1],)
+                    self.__cuda_array_interface__ = cai
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: Move this to the top-level so we don't define it every time?
created_at: 2025-04-02T15:13:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +317,157 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            try:
+                import numpy as np
+            except ImportError:
+                raise ImportError(
+                    "NumPy must be installed to use this method on 2D data"
+                )
+            num_rows, num_cols = shape
+            if num_rows < numeric_limits[size_type].max():
+                # TODO: Add a new Scalar.from_py(value, dtype) API so
+                # we can specify the dtype (size_type in this case) too.
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds size_type limit for offsets column."
+                )
+
+            class _Ravelled:
+                def __init__(self, obj):
+                    self.obj = obj
+                    cai = obj.__cuda_array_interface__.copy()
+                    shape = cai["shape"]
+                    cai["shape"] = (shape[0]*shape[1],)
+                    self.__cuda_array_interface__ = cai
+            rav_obj = _Ravelled(obj)
+            data_col = cls(
+                data_type=data_type,
+                size=rav_obj.__cuda_array_interface__["shape"][0],
+                data=gpumemoryview(rav_obj),
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
+            return cls(
+                data_type=DataType(type_id.LIST),
+                size=num_rows,
+                data=None,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[offsets_col, data_col],
+            )
+        else:
+            raise ValueError("Only 1D or 2D arrays are supported")
+
+    @classmethod
+    def from_array(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy
+        or CUDA array interface.
+
+        Parameters
+        ----------
+        obj : object
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input does not implement a supported array interface.
+        ImportError
+            If NumPy or CuPy is required but not installed.
+
+        Notes
+        -----
+        - For `cupy.ndarray`, this uses the CUDA array interface and
+          supports 1D or 2D arrays.
path: python/pylibcudf/pylibcudf/column.pyx
user: login: wence-
body: ```suggestion
        - 1D and 2D C-contiguous device arrays are supported. 
          The data are not copied.
```
created_at: 2025-04-02T15:14:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +327,149 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
path: python/pylibcudf/pylibcudf/column.pyx
user: login: vyasr
body: This is a classic case where we should prefer a try-except. It'll be faster in the majority of relevant cases where someone passes an object that actually has a CAI. If a user passes something that does not, we're going to error any way, so we can afford the overhead of exception handling. 
```suggestion
        try:
            iface = obj.__cuda_array_interface__
        except AttributeError:
            raise TypeError("Object does not implement __cuda_array_interface__")
```
created_at: 2025-04-02T17:30:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +327,149 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
         """
-        data = gpumemoryview(obj)
-        iface = data.__cuda_array_interface__
-        if iface.get('mask') is not None:
-            raise ValueError("mask not yet supported.")
+        iface = getattr(obj, "__cuda_array_interface__", None)
+        if iface is None:
+            raise TypeError("Object does not implement __cuda_array_interface__")
 
-        typestr = iface['typestr'][1:]
+        if iface.get("mask") is not None:
+            raise ValueError("mask not yet supported")
+
+        typestr = iface["typestr"][1:]
         data_type = _datatype_from_dtype_desc(typestr)
 
-        if not is_c_contiguous(
-            iface['shape'],
-            iface['strides'],
-            size_of(data_type)
-        ):
+        shape = iface["shape"]
+        if not is_c_contiguous(shape, iface["strides"], size_of(data_type)):
             raise ValueError("Data must be C-contiguous")
 
-        size = iface['shape'][0]
-        return Column(
-            data_type,
-            size,
-            data,
-            None,
-            0,
-            0,
-            []
+        if len(shape) == 1:
+            data = gpumemoryview(obj)
+            size = shape[0]
+            return cls(data_type, size, data, None, 0, 0, [])
+        elif len(shape) == 2:
+            try:
+                import numpy as np
+            except ImportError:
+                raise ImportError(
+                    "NumPy must be installed to use this method on 2D data"
+                )
+            num_rows, num_cols = shape
+            if num_rows < numeric_limits[size_type].max():
+                # TODO: Add a new Scalar.from_py(value, dtype) API so
+                # we can specify the dtype (size_type in this case) too.
+                offsets_col = sequence(
+                    num_rows + 1,
+                    Scalar.from_numpy(np.int32(0)),
+                    Scalar.from_numpy(np.int32(num_cols))
+                )
+            else:
+                raise ValueError(
+                    "Number of rows exceeds size_type limit for offsets column."
+                )
+            rav_obj = _Ravelled(obj)
+            data_col = cls(
+                data_type=data_type,
+                size=rav_obj.__cuda_array_interface__["shape"][0],
+                data=gpumemoryview(rav_obj),
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[],
+            )
+            return cls(
+                data_type=DataType(type_id.LIST),
+                size=num_rows,
+                data=None,
+                mask=None,
+                null_count=0,
+                offset=0,
+                children=[offsets_col, data_col],
+            )
+        else:
+            raise ValueError("Only 1D or 2D arrays are supported")
+
+    @classmethod
+    def from_array(cls, obj):
+        """
+        Create a Column from any object which supports the NumPy
+        or CUDA array interface.
+
+        Parameters
+        ----------
+        obj : object
+            The input array to be converted into a `pylibcudf.Column`.
+
+        Returns
+        -------
+        Column
+
+        Raises
+        ------
+        TypeError
+            If the input does not implement a supported array interface.
+        ImportError
+            If NumPy or CuPy is required but not installed.
path: python/pylibcudf/pylibcudf/column.pyx
user: login: vyasr
body: Same question as above regarding cupy (it looks like we do use numpy though).
created_at: 2025-04-02T17:34:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,50 +327,149 @@ cdef class Column:
             c_result = make_column_from_scalar(dereference(slr.get()), size)
         return Column.from_libcudf(move(c_result))
 
-    @staticmethod
-    def from_cuda_array_interface_obj(object obj):
-        """Create a Column from an object with a CUDA array interface.
+    @classmethod
+    def from_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the NumPy Array Interface.
+
+        Parameters
+        ----------
+        obj : object
+            Must implement the `__array_interface__` protocol.
+
+        Raises
+        ------
+        NotImplementedError
+            This method is not yet implemented.
+        """
+        raise NotImplementedError(
+            "Converting to a pylibcudf Column is not yet implemented."
+        )
+
+    @classmethod
+    def from_cuda_array_interface(cls, obj):
+        """
+        Create a Column from an object implementing the CUDA Array Interface.
 
         Parameters
         ----------
         obj : object
-            The object with the CUDA array interface to create a column from.
+            Must implement the ``__cuda_array_interface__`` protocol.
 
         Returns
         -------
         Column
             A Column containing the data from the CUDA array interface.
 
-        Notes
-        -----
-        Data is not copied when creating the column. The caller is
-        responsible for ensuring the data is not mutated unexpectedly while the
-        column is in use.
+        Raises
+        ------
+        ValueError
+            If the object is not 1D or 2D, or is not C-contiguous.
+        ImportError
+            If CuPy is not installed.
path: python/pylibcudf/pylibcudf/column.pyx
user: login: vyasr
body: Is this right? Do we ever actually require cupy for this? It seems like this should be numpy.
```suggestion
            If NumPy is not installed.
```
created_at: 2025-04-02T17:34:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: Matt711
created_at: 2025-03-20T13:12:46Z
body: Narwhals failures are unrelated to this PR
author_association: CONTRIBUTOR
user: login: copy-pr-bot[bot]
created_at: 2025-03-31T18:03:33Z
body: Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message [here](https://docs.gha-runners.nvidia.com/cpr/auto-sync).

author_association: NONE
user: login: vyasr
created_at: 2025-04-02T17:36:28Z
body: I left some suggestions but I don't need to review again. Thanks!
author_association: CONTRIBUTOR
user: login: Matt711
created_at: 2025-04-02T18:39:01Z
body: /merge
author_association: CONTRIBUTOR