Repository: narwhals-dev/narwhals
Pull request #436 (2024-07-08 08:20:38)

feat: allow dt.microsecond and dt.millisecond in old pyarrow-backed p…
…andas


<!--
# 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/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 8ec40161..40082c5f 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -15,6 +15,7 @@ from narwhals._pandas_like.utils import validate_column_comparand
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_pandas
+from narwhals.dependencies import get_pyarrow_compute
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -657,31 +658,30 @@ class PandasSeriesDateTimeNamespace:
         )
 
     def millisecond(self) -> PandasSeries:
-        if "pyarrow" in str(self._series._native_series.dtype):
-            msg = ".dt.millisecond not implemented for pyarrow-backed pandas"
-            raise NotImplementedError(msg)
-        return self._series._from_native_series(
-            self._series._native_series.dt.microsecond // 1000,
-        )
+        return self.microsecond() // 1000
 
     def microsecond(self) -> PandasSeries:
-        if "pyarrow" in str(self._series._native_series.dtype):
-            msg = ".dt.microsecond not implemented for pyarrow-backed pandas"
-            raise NotImplementedError(msg)
+        if self._series._backend_version < (3, 0, 0) and "pyarrow" in str(
+            self._series._native_series.dtype
+        ):
+            # crazy workaround for https://github.com/pandas-dev/pandas/issues/59154
+            pc = get_pyarrow_compute()
+            native_series = self._series._native_series
+            arr = native_series.array.__arrow_array__()
+            result_arr = pc.add(
+                pc.multiply(pc.millisecond(arr), 1000), pc.microsecond(arr)
+            )
+            result = native_series.__class__(
+                native_series.array.__class__(result_arr), name=native_series.name
+            )
+            return self._series._from_native_series(result)
+
         return self._series._from_native_series(
             self._series._native_series.dt.microsecond
         )
 
     def nanosecond(self) -> PandasSeries:
-        if "pyarrow" in str(self._series._native_series.dtype):
-            msg = ".dt.nanosecond not implemented for pyarrow-backed pandas"
-            raise NotImplementedError(msg)
-        return self._series._from_native_series(
-            (
-                (self._series._native_series.dt.microsecond * 1_000)
-                + self._series._native_series.dt.nanosecond
-            ),
-        )
+        return self.microsecond() * 1_000 + self._series._native_series.dt.nanosecond  # type: ignore[no-any-return]
 
     def ordinal_day(self) -> PandasSeries:
         ser = self._series._native_series


Test patch:
diff --git a/tests/expr/test_dt.py b/tests/expr/test_dt.py
index 18ab04de..552df753 100644
--- a/tests/expr/test_dt.py
+++ b/tests/expr/test_dt.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import contextlib
 from datetime import datetime
 from datetime import timedelta
 from typing import Any
@@ -57,21 +56,11 @@ def test_datetime_attributes(
     expected: list[int],
     constructor: Any,
 ) -> None:
-    if "pyarrow" in str(constructor) and attribute in {
-        "millisecond",
-        "microsecond",
-        "nanosecond",
-    }:
-        ctx: Any = pytest.raises(NotImplementedError, match="pyarrow")
-    else:
-        ctx = contextlib.nullcontext()
     df = nw.from_native(constructor(data), eager_only=True)
-    with ctx:
-        result = nw.to_native(df.select(getattr(nw.col("a").dt, attribute)()))
-        compare_dicts(result, {"a": expected})
-    with ctx:
-        result = nw.to_native(df.select(getattr(df["a"].dt, attribute)()))
-        compare_dicts(result, {"a": expected})
+    result = nw.to_native(df.select(getattr(nw.col("a").dt, attribute)()))
+    compare_dicts(result, {"a": expected})
+    result = nw.to_native(df.select(getattr(df["a"].dt, attribute)()))
+    compare_dicts(result, {"a": expected})
 
 
 @pytest.mark.parametrize(