Repository: narwhals-dev/narwhals
Pull request #708 (2024-08-02 11:46:13)

feat: dask equality and inequality operators
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue #637 

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.

How did we survive for so long without these?


Patch:
diff --git a/docs/how_it_works.md b/docs/how_it_works.md
index beecdda8..40740f7a 100644
--- a/docs/how_it_works.md
+++ b/docs/how_it_works.md
@@ -19,7 +19,7 @@ def col_a(df):
 Let's step up the complexity. How about `nw.col('a')+1`? We already know what the
 `nw.col('a')` part looks like, so we just need to add `1` to each of its outputs:
 
-```python exec="1"
+```python exec="1" source="above"
 def col_a(df):
     return [df.loc[:, "a"]]
 
@@ -30,7 +30,7 @@ def col_a_plus_1(df):
 
 Expressions can return multiple Series - for example, `nw.col('a', 'b')` translates to:
 
-```python exec="1"
+```python exec="1" source="above"
 def col_a_b(df):
     return [df.loc[:, "a"], df.loc[:, "b"]]
 ```
@@ -38,7 +38,7 @@ def col_a_b(df):
 Expressions can also take multiple columns as input - for example, `nw.sum_horizontal('a', 'b')`
 translates to:
 
-```python exec="1"
+```python exec="1" source="above"
 def sum_horizontal_a_b(df):
     return [df.loc[:, "a"] + df.loc[:, "b"]]
 ```
diff --git a/mkdocs.yml b/mkdocs.yml
index ba94c429..5c8549fc 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -45,12 +45,15 @@ nav:
 theme:
   name: material
   font: false
+  favicon: assets/image.png
+  logo: assets/image.png
   features:
     - content.code.copy
     - content.code.annotate
     - navigation.footer
     - navigation.indexes
-  palette:
+    - navigation.top
+  palette:   
     # Palette toggle for automatic mode
     - media: "(prefers-color-scheme)"
       toggle:
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 7e7a0d20..1fcfd528 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -182,6 +182,9 @@ class ArrowExpr:
     def cum_sum(self) -> Self:
         return reuse_series_implementation(self, "cum_sum")
 
+    def round(self, decimals: int) -> Self:
+        return reuse_series_implementation(self, "round", decimals)
+
     def any(self) -> Self:
         return reuse_series_implementation(self, "any", returns_scalar=True)
 
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 47910867..731db14a 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -288,6 +288,12 @@ class ArrowSeries:
         pc = get_pyarrow_compute()
         return self._from_native_series(pc.cumulative_sum(self._native_series))
 
+    def round(self, decimals: int) -> Self:
+        pc = get_pyarrow_compute()
+        return self._from_native_series(
+            pc.round(self._native_series, decimals, round_mode="half_towards_infinity")
+        )
+
     def diff(self) -> Self:
         pc = get_pyarrow_compute()
         return self._from_native_series(
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index bb2ab4e1..28db2a5c 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -154,6 +154,20 @@ class DaskExpr:
             other,
         )
 
+    def __eq__(self, other: DaskExpr) -> Self:  # type: ignore[override]
+        return self._from_call(
+            lambda _input, other: _input.__eq__(other),
+            "__eq__",
+            other,
+        )
+
+    def __ne__(self, other: DaskExpr) -> Self:  # type: ignore[override]
+        return self._from_call(
+            lambda _input, other: _input.__ne__(other),
+            "__ne__",
+            other,
+        )
+
     def __ge__(self, other: DaskExpr) -> Self:
         return self._from_call(
             lambda _input, other: _input.__ge__(other),
diff --git a/narwhals/expr.py b/narwhals/expr.py
index b2049357..195c6470 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1638,12 +1638,12 @@ class Expr:
             decimals: Number of decimals to round by.
 
         Notes:
-            For values exactly halfway between rounded decimal values pandas and Polars behave differently.
+            For values exactly halfway between rounded decimal values pandas behaves differently than Polars and Arrow.
 
             pandas rounds to the nearest even value (e.g. -0.5 and 0.5 round to 0.0, 1.5 and 2.5 round to 2.0, 3.5 and
             4.5 to 4.0, etc..).
 
-            Polars rounds away from 0 (e.g. -0.5 to -1.0, 0.5 to 1.0, 1.5 to 2.0, 2.5 to 3.0, etc..).
+            Polars and Arrow round away from 0 (e.g. -0.5 to -1.0, 0.5 to 1.0, 1.5 to 2.0, 2.5 to 3.0, etc..).
 
 
         Examples:
diff --git a/narwhals/series.py b/narwhals/series.py
index 27a59472..7caee0c7 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1934,12 +1934,12 @@ class Series:
             decimals: Number of decimals to round by.
 
         Notes:
-            For values exactly halfway between rounded decimal values pandas and Polars behave differently.
+            For values exactly halfway between rounded decimal values pandas behaves differently than Polars and Arrow.
 
             pandas rounds to the nearest even value (e.g. -0.5 and 0.5 round to 0.0, 1.5 and 2.5 round to 2.0, 3.5 and
             4.5 to 4.0, etc..).
 
-            Polars rounds away from 0 (e.g. -0.5 to -1.0, 0.5 to 1.0, 1.5 to 2.0, 2.5 to 3.0, etc..).
+            Polars and Arrow round away from 0 (e.g. -0.5 to -1.0, 0.5 to 1.0, 1.5 to 2.0, 2.5 to 3.0, etc..).
 
         Examples:
             >>> import narwhals as nw


Test patch:
diff --git a/tests/expr_and_series/operators_test.py b/tests/expr_and_series/operators_test.py
index 10134a9d..585b8c47 100644
--- a/tests/expr_and_series/operators_test.py
+++ b/tests/expr_and_series/operators_test.py
@@ -19,17 +19,35 @@ from tests.utils import compare_dicts
         ("__gt__", [False, False, True]),
     ],
 )
-def test_comparand_operators(
-    constructor: Any, operator: str, expected: list[bool], request: Any
+def test_comparand_operators_scalar(
+    constructor: Any, operator: str, expected: list[bool]
 ) -> None:
-    if "dask" in str(constructor) and operator in {"__eq__", "__ne__"}:
-        request.applymarker(pytest.mark.xfail)
     data = {"a": [0, 1, 2]}
     df = nw.from_native(constructor(data))
     result = df.select(getattr(nw.col("a"), operator)(1))
     compare_dicts(result, {"a": expected})
 
 
+@pytest.mark.parametrize(
+    ("operator", "expected"),
+    [
+        ("__eq__", [True, False, False]),
+        ("__ne__", [False, True, True]),
+        ("__le__", [True, False, True]),
+        ("__lt__", [False, False, True]),
+        ("__ge__", [True, True, False]),
+        ("__gt__", [False, True, False]),
+    ],
+)
+def test_comparand_operators_expr(
+    constructor: Any, operator: str, expected: list[bool]
+) -> None:
+    data = {"a": [0, 1, 1], "b": [0, 0, 2]}
+    df = nw.from_native(constructor(data))
+    result = df.select(getattr(nw.col("a"), operator)(nw.col("b")))
+    compare_dicts(result, {"a": expected})
+
+
 @pytest.mark.parametrize(
     ("operator", "expected"),
     [
diff --git a/tests/expr_and_series/round_test.py b/tests/expr_and_series/round_test.py
index 0c42faf3..cec8ea51 100644
--- a/tests/expr_and_series/round_test.py
+++ b/tests/expr_and_series/round_test.py
@@ -12,9 +12,7 @@ from tests.utils import compare_dicts
 def test_round(request: Any, constructor: Any, decimals: int) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-    data = {"a": [1.12345, 2.56789, 3.901234]}
+    data = {"a": [2.12345, 2.56789, 3.901234]}
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
@@ -24,9 +22,7 @@ def test_round(request: Any, constructor: Any, decimals: int) -> None:
 
 
 @pytest.mark.parametrize("decimals", [0, 1, 2])
-def test_round_series(request: Any, constructor_eager: Any, decimals: int) -> None:
-    if "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
+def test_round_series(constructor_eager: Any, decimals: int) -> None:
     data = {"a": [1.12345, 2.56789, 3.901234]}
     df_raw = constructor_eager(data)
     df = nw.from_native(df_raw, eager_only=True)