Repository: narwhals-dev/narwhals
Pull request #534 (2024-07-16 06:29:57)

feat: add `is_null` and `__invert__` to pyarrow series
<!--
# 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
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # https://github.com/narwhals-dev/narwhals/issues/372
- Closes #

## Checklist

- [x] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.

I also added __invert__ so that we can reuse the test in `test_common.py`
Please let me know 🙏


Patch:
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 9635b835..bf3cc7de 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -125,6 +125,9 @@ class ArrowExpr:
     def __rpow__(self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__rpow__", other)
 
+    def __invert__(self) -> Self:
+        return reuse_series_implementation(self, "__invert__")
+
     def filter(self, *predicates: Any) -> Self:
         from narwhals._arrow.namespace import ArrowNamespace
 
@@ -183,6 +186,9 @@ class ArrowExpr:
     def null_count(self) -> Self:
         return reuse_series_implementation(self, "null_count", returns_scalar=True)
 
+    def is_null(self) -> Self:
+        return reuse_series_implementation(self, "is_null")
+
     def head(self, n: int) -> Self:
         return reuse_series_implementation(self, "head", n)
 
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 5d1ae9a9..b0f2b6ae 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -139,6 +139,10 @@ class ArrowSeries:
         other = validate_column_comparand(other)
         return self._from_native_series(pc.power(other, ser))
 
+    def __invert__(self) -> Self:
+        pc = get_pyarrow_compute()
+        return self._from_native_series(pc.invert(self._native_series))
+
     def filter(self, other: Any) -> Self:
         other = validate_column_comparand(other)
         return self._from_native_series(self._native_series.filter(other))
@@ -229,6 +233,10 @@ class ArrowSeries:
     def is_empty(self) -> bool:
         return len(self) == 0
 
+    def is_null(self) -> Self:
+        ser = self._native_series
+        return self._from_native_series(ser.is_null())
+
     def cast(self, dtype: DType) -> Self:
         pc = get_pyarrow_compute()
         ser = self._native_series
diff --git a/utils/check_backend_completeness.py b/utils/check_backend_completeness.py
index ab2eec28..f412fc5e 100644
--- a/utils/check_backend_completeness.py
+++ b/utils/check_backend_completeness.py
@@ -25,7 +25,6 @@ MISSING = [
     "Series.is_first_distinct",
     "Series.is_in",
     "Series.is_last_distinct",
-    "Series.is_null",
     "Series.is_sorted",
     "Series.is_unique",
     "Series.len",


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
new file mode 100644
index 00000000..9a587f02
--- /dev/null
+++ b/.github/workflows/downstream_tests.yml
@@ -0,0 +1,89 @@
+name: ci
+
+on:
+  pull_request:
+  push:
+    branches: [main]
+
+jobs:
+  altair:
+    strategy:
+      matrix:
+        python-version: ["3.11"]
+        os: [ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Cache multiple paths
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cache/pip
+            $RUNNER_TOOL_CACHE/Python/*
+            ~\AppData\Local\pip\Cache
+          key: ${{ runner.os }}-build-${{ matrix.python-version }}
+      - name: clone-altair
+        run: git clone https://github.com/vega/altair.git
+      - name: install-basics
+        run: python -m pip install --upgrade tox virtualenv setuptools pip
+      - name: install-altair-dev
+        run: |
+            cd altair
+            pip install -e ".[dev, all]"
+      - name: install-narwhals-dev
+        run: |
+            pip uninstall narwhals -y
+            pip install -e .
+      - name: show-deps
+        run: pip freeze
+      - name: Run pytest
+        run: |
+            cd altair
+            pytest tests
+      - name: Run mypy
+        run: |
+            cd altair
+            mypy altair tests
+
+  scikit-lego:
+    strategy:
+      matrix:
+        python-version: ["3.11"]
+        os: [ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Cache multiple paths
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cache/pip
+            $RUNNER_TOOL_CACHE/Python/*
+            ~\AppData\Local\pip\Cache
+          key: ${{ runner.os }}-build-${{ matrix.python-version }}
+      - name: clone-scikit-lego
+        run: git clone https://github.com/koaning/scikit-lego.git
+      - name: install-basics
+        run: python -m pip install --upgrade tox virtualenv setuptools pip
+      - name: install-scikit-lego-dev
+        run: |
+            cd scikit-lego
+            pip install -e ".[dev, test]"
+      - name: install-narwhals-dev
+        run: |
+            pip uninstall narwhals -y
+            pip install -e .
+      - name: show-deps
+        run: pip freeze
+      - name: Run pytest
+        run: |
+            cd scikit-lego
+            pytest -n auto --disable-warnings --cov=sklego -m "not cvxpy and not formulaic and not umap"
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index 01eb3495..03efae10 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -152,10 +152,7 @@ def test_expr_min_max(request: Any, constructor: Any) -> None:
     compare_dicts(result_max, expected_max)
 
 
-def test_expr_na(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
-        request.applymarker(pytest.mark.xfail)
-
+def test_expr_na(constructor_with_lazy: Any) -> None:
     df = nw.from_native(constructor_with_lazy(data_na)).lazy()
     result_nna = df.filter((~nw.col("a").is_null()) & (~df.collect()["z"].is_null()))
     expected = {"a": [2], "b": [6], "z": [9]}