Repository: narwhals-dev/narwhals
Pull request #1104 (2024-09-30 20:32:31)

feat: allow inspecting the inner type of List
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # 
- Closes #

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.




Patch:
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index e34e949d..bb5f3f7f 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -58,7 +58,7 @@ def native_to_narwhals_dtype(dtype: Any, dtypes: DTypes) -> DType:
     if pa.types.is_struct(dtype):
         return dtypes.Struct()
     if pa.types.is_list(dtype) or pa.types.is_large_list(dtype):
-        return dtypes.List()
+        return dtypes.List(native_to_narwhals_dtype(dtype.value_type, dtypes))
     if pa.types.is_fixed_size_list(dtype):
         return dtypes.Array()
     return dtypes.Unknown()  # pragma: no cover
diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py
index 5e8cb73d..95567283 100644
--- a/narwhals/_duckdb/dataframe.py
+++ b/narwhals/_duckdb/dataframe.py
@@ -50,8 +50,8 @@ def map_duckdb_dtype_to_narwhals_dtype(duckdb_dtype: Any, dtypes: DTypes) -> DTy
         return dtypes.Duration()
     if duckdb_dtype.startswith("STRUCT"):
         return dtypes.Struct()
-    if re.match(r"\w+\[\]", duckdb_dtype):
-        return dtypes.List()
+    if match_ := re.match(r"(.*)\[\]$", duckdb_dtype):
+        return dtypes.List(map_duckdb_dtype_to_narwhals_dtype(match_.group(1), dtypes))
     if re.match(r"\w+\[\d+\]", duckdb_dtype):
         return dtypes.Array()
     return dtypes.Unknown()
diff --git a/narwhals/_ibis/dataframe.py b/narwhals/_ibis/dataframe.py
index 6f53e277..8ee01e78 100644
--- a/narwhals/_ibis/dataframe.py
+++ b/narwhals/_ibis/dataframe.py
@@ -43,7 +43,9 @@ def map_ibis_dtype_to_narwhals_dtype(ibis_dtype: Any, dtypes: DTypes) -> DType:
     if ibis_dtype.is_timestamp():
         return dtypes.Datetime()
     if ibis_dtype.is_array():
-        return dtypes.List()
+        return dtypes.List(
+            map_ibis_dtype_to_narwhals_dtype(ibis_dtype.value_type, dtypes)
+        )
     if ibis_dtype.is_struct():
         return dtypes.Struct()
     return dtypes.Unknown()  # pragma: no cover
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 92fbd519..910c19a1 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -7,6 +7,9 @@ from typing import Iterable
 from typing import Literal
 from typing import TypeVar
 
+from narwhals._arrow.utils import (
+    native_to_narwhals_dtype as arrow_native_to_narwhals_dtype,
+)
 from narwhals.utils import Implementation
 from narwhals.utils import isinstance_or_issubclass
 
@@ -276,7 +279,9 @@ def native_to_narwhals_dtype(column: Any, dtypes: DTypes) -> DType:
     if dtype == "date32[day][pyarrow]":
         return dtypes.Date()
     if dtype.startswith(("large_list", "list")):
-        return dtypes.List()
+        return dtypes.List(
+            arrow_native_to_narwhals_dtype(column.dtype.pyarrow_dtype.value_type, dtypes)
+        )
     if dtype.startswith("fixed_size_list"):
         return dtypes.Array()
     if dtype.startswith("struct"):
diff --git a/narwhals/_polars/utils.py b/narwhals/_polars/utils.py
index b2f06090..95b98b17 100644
--- a/narwhals/_polars/utils.py
+++ b/narwhals/_polars/utils.py
@@ -75,7 +75,7 @@ def native_to_narwhals_dtype(dtype: Any, dtypes: DTypes) -> DType:
     if dtype == pl.Struct:
         return dtypes.Struct()
     if dtype == pl.List:
-        return dtypes.List()
+        return dtypes.List(native_to_narwhals_dtype(dtype.inner, dtypes))
     if dtype == pl.Array:
         return dtypes.Array()
     return dtypes.Unknown()
diff --git a/narwhals/dtypes.py b/narwhals/dtypes.py
index 730f6984..6acfbb76 100644
--- a/narwhals/dtypes.py
+++ b/narwhals/dtypes.py
@@ -173,7 +173,31 @@ class Enum(DType): ...
 class Struct(DType): ...
 
 
-class List(DType): ...
+class List(DType):
+    def __init__(self, inner: DType | type[DType]) -> None:
+        self.inner = inner
+
+    def __eq__(self, other: DType | type[DType]) -> bool:  # type: ignore[override]
+        # This equality check allows comparison of type classes and type instances.
+        # If a parent type is not specific about its inner type, we infer it as equal:
+        # > list[i64] == list[i64] -> True
+        # > list[i64] == list[f32] -> False
+        # > list[i64] == list      -> True
+
+        # allow comparing object instances to class
+        if type(other) is type and issubclass(other, self.__class__):
+            return True
+        elif isinstance(other, self.__class__):
+            return self.inner == other.inner
+        else:
+            return False
+
+    def __hash__(self) -> int:
+        return hash((self.__class__, self.inner))
+
+    def __repr__(self) -> str:
+        class_name = self.__class__.__name__
+        return f"{class_name}({self.inner!r})"
 
 
 class Array(DType): ...


Test patch:
diff --git a/tests/dtypes_test.py b/tests/dtypes_test.py
index 58061597..c64dfed5 100644
--- a/tests/dtypes_test.py
+++ b/tests/dtypes_test.py
@@ -53,7 +53,21 @@ def test_duration_invalid(time_unit: str) -> None:
         nw.Duration(time_unit=time_unit)  # type: ignore[arg-type]
 
 
-def test_second_tu() -> None:
+def test_list_valid() -> None:
+    dtype = nw.List(nw.Int64)
+    assert dtype == nw.List(nw.Int64)
+    assert dtype == nw.List
+    assert dtype != nw.List(nw.Float32)
+    assert dtype != nw.Duration
+    assert repr(dtype) == "List(<class 'narwhals.dtypes.Int64'>)"
+    dtype = nw.List(nw.List(nw.Int64))
+    assert dtype == nw.List(nw.List(nw.Int64))
+    assert dtype == nw.List
+    assert dtype != nw.List(nw.List(nw.Float32))
+    assert dtype in {nw.List(nw.List(nw.Int64))}
+
+
+def test_second_time_unit() -> None:
     s = pd.Series(np.array([np.datetime64("2020-01-01", "s")]))
     result = nw.from_native(s, series_only=True)
     if parse_version(pd.__version__) < (2,):  # pragma: no cover