Repository: narwhals-dev/narwhals
Pull request #652 (2024-07-28 09:12:10)

chore: split eager-only and eager+lazy tests a bit
<!--
# 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/expr.py b/narwhals/_arrow/expr.py
index 792a1ea3..7ec20357 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -100,9 +100,15 @@ class ArrowExpr:
     def __and__(self, other: ArrowExpr | bool | Any) -> Self:
         return reuse_series_implementation(self, "__and__", other=other)
 
+    def __rand__(self, other: ArrowExpr | bool | Any) -> Self:
+        return reuse_series_implementation(self, "__rand__", other=other)
+
     def __or__(self, other: ArrowExpr | bool | Any) -> Self:
         return reuse_series_implementation(self, "__or__", other=other)
 
+    def __ror__(self, other: ArrowExpr | bool | Any) -> Self:
+        return reuse_series_implementation(self, "__ror__", other=other)
+
     def __add__(self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__add__", other)
 
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 7b396c22..47910867 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -106,12 +106,24 @@ class ArrowSeries:
         other = validate_column_comparand(other)
         return self._from_native_series(pc.and_kleene(ser, other))
 
+    def __rand__(self, other: Any) -> Self:
+        pc = get_pyarrow_compute()
+        ser = self._native_series
+        other = validate_column_comparand(other)
+        return self._from_native_series(pc.and_kleene(other, ser))
+
     def __or__(self, other: Any) -> Self:
         pc = get_pyarrow_compute()
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.or_kleene(ser, other))
 
+    def __ror__(self, other: Any) -> Self:
+        pc = get_pyarrow_compute()
+        ser = self._native_series
+        other = validate_column_comparand(other)
+        return self._from_native_series(pc.or_kleene(other, ser))
+
     def __add__(self, other: Any) -> Self:
         pc = get_pyarrow_compute()
         other = validate_column_comparand(other)
diff --git a/pyproject.toml b/pyproject.toml
index ea65b304..ff054250 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -87,6 +87,8 @@ filterwarnings = [
   'ignore:np.find_common_type is deprecated',
   'ignore:is_sparse is deprecated and will be removed',
   'ignore:Passing a BlockManager to DataFrame is deprecated',
+  'ignore:.*defaulting to pandas implementation',
+  'ignore:.*implementation has mismatches with pandas',
 ]
 xfail_strict = true
 markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index da68aa82..5a213550 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -28,8 +28,6 @@ jobs:
           key: ${{ runner.os }}-build-${{ matrix.python-version }}
       - name: install-reqs
         run: python -m pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt
-      - name: install-modin
-        run: python -m pip install --upgrade modin[dask]
       - name: show-deps
         run: pip freeze
       - name: Run pytest
diff --git a/tests/conftest.py b/tests/conftest.py
index 992d1896..db8afeed 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,4 @@
+import contextlib
 from typing import Any
 from typing import Callable
 
@@ -6,10 +7,17 @@ import polars as pl
 import pyarrow as pa
 import pytest
 
+from narwhals.dependencies import get_dask_dataframe
 from narwhals.dependencies import get_modin
 from narwhals.typing import IntoDataFrame
+from narwhals.typing import IntoFrame
 from narwhals.utils import parse_version
 
+with contextlib.suppress(ImportError):
+    import modin.pandas  # noqa: F401
+with contextlib.suppress(ImportError):
+    import dask.dataframe  # noqa: F401
+
 
 def pytest_addoption(parser: Any) -> None:
     parser.addoption(
@@ -45,7 +53,7 @@ def pandas_pyarrow_constructor(obj: Any) -> IntoDataFrame:
 
 def modin_constructor(obj: Any) -> IntoDataFrame:  # pragma: no cover
     mpd = get_modin()
-    return mpd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow")  # type: ignore[no-any-return]
+    return mpd.DataFrame(pd.DataFrame(obj)).convert_dtypes(dtype_backend="pyarrow")  # type: ignore[no-any-return]
 
 
 def polars_eager_constructor(obj: Any) -> IntoDataFrame:
@@ -56,6 +64,11 @@ def polars_lazy_constructor(obj: Any) -> pl.LazyFrame:
     return pl.LazyFrame(obj)
 
 
+def dask_lazy_constructor(obj: Any) -> IntoFrame:  # pragma: no cover
+    dd = get_dask_dataframe()
+    return dd.from_pandas(pd.DataFrame(obj))  # type: ignore[no-any-return]
+
+
 def pyarrow_table_constructor(obj: Any) -> IntoDataFrame:
     return pa.table(obj)  # type: ignore[no-any-return]
 
@@ -70,9 +83,13 @@ else:  # pragma: no cover
     eager_constructors = [pandas_constructor]
 
 eager_constructors.extend([polars_eager_constructor, pyarrow_table_constructor])
+lazy_constructors = [polars_lazy_constructor]
 
 if get_modin() is not None:  # pragma: no cover
     eager_constructors.append(modin_constructor)
+# TODO(unassigned): when Dask gets better support, remove the "False and" part
+if False and get_dask_dataframe() is not None:  # pragma: no cover  # noqa: SIM223
+    lazy_constructors.append(dask_lazy_constructor)
 
 
 @pytest.fixture(params=eager_constructors)
@@ -80,6 +97,6 @@ def constructor(request: Any) -> Callable[[Any], IntoDataFrame]:
     return request.param  # type: ignore[no-any-return]
 
 
-@pytest.fixture(params=[*eager_constructors, polars_lazy_constructor])
-def constructor_with_lazy(request: Any) -> Callable[[Any], Any]:
+@pytest.fixture(params=[*eager_constructors, *lazy_constructors])
+def constructor_lazy(request: Any) -> Callable[[Any], Any]:
     return request.param  # type: ignore[no-any-return]
diff --git a/tests/expr_and_series/abs_test.py b/tests/expr_and_series/abs_test.py
index ea038ead..8fc4b730 100644
--- a/tests/expr_and_series/abs_test.py
+++ b/tests/expr_and_series/abs_test.py
@@ -4,10 +4,15 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_abs(constructor: Any) -> None:
-    df = nw.from_native(constructor({"a": [1, 2, 3, -4, 5]}), eager_only=True)
+def test_abs(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy({"a": [1, 2, 3, -4, 5]}))
     result = df.select(b=nw.col("a").abs())
     expected = {"b": [1, 2, 3, 4, 5]}
     compare_dicts(result, expected)
-    result = df.select(b=df["a"].abs())
+
+
+def test_abs_series(constructor: Any) -> None:
+    df = nw.from_native(constructor({"a": [1, 2, 3, -4, 5]}), eager_only=True)
+    result = {"b": df["a"].abs()}
+    expected = {"b": [1, 2, 3, 4, 5]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py
index c34b9b9e..fb06cb37 100644
--- a/tests/expr_and_series/all_horizontal_test.py
+++ b/tests/expr_and_series/all_horizontal_test.py
@@ -1,20 +1,28 @@
 from typing import Any
 
-import numpy as np
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-@pytest.mark.parametrize("col_expr", [np.array([False, False, True]), nw.col("a"), "a"])
-def test_allh(constructor: Any, col_expr: Any) -> None:
+def test_allh(constructor_lazy: Any) -> None:
+    data = {
+        "a": [False, False, True],
+        "b": [False, True, True],
+    }
+    df = nw.from_native(constructor_lazy(data))
+    result = df.select(all=nw.all_horizontal(nw.col("a"), nw.col("b")))
+
+    expected = {"all": [False, False, True]}
+    compare_dicts(result, expected)
+
+
+def test_allh_series(constructor: Any) -> None:
     data = {
         "a": [False, False, True],
         "b": [False, True, True],
     }
     df = nw.from_native(constructor(data), eager_only=True)
-    result = df.select(all=nw.all_horizontal(col_expr, nw.col("b")))
+    result = df.select(all=nw.all_horizontal(df["a"], df["b"]))
 
     expected = {"all": [False, False, True]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/any_all_test.py b/tests/expr_and_series/any_all_test.py
index 9ed8d31b..0872d73e 100644
--- a/tests/expr_and_series/any_all_test.py
+++ b/tests/expr_and_series/any_all_test.py
@@ -4,9 +4,9 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_any_all(constructor: Any) -> None:
+def test_any_all(constructor_lazy: Any) -> None:
     df = nw.from_native(
-        constructor(
+        constructor_lazy(
             {
                 "a": [True, False, True],
                 "b": [True, True, True],
diff --git a/tests/expr_and_series/any_horizontal_test.py b/tests/expr_and_series/any_horizontal_test.py
index df5b35eb..31d771e5 100644
--- a/tests/expr_and_series/any_horizontal_test.py
+++ b/tests/expr_and_series/any_horizontal_test.py
@@ -1,20 +1,16 @@
 from typing import Any
 
-import numpy as np
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-@pytest.mark.parametrize("col_expr", [np.array([False, False, True]), nw.col("a"), "a"])
-def test_anyh(constructor: Any, col_expr: Any) -> None:
+def test_anyh(constructor_lazy: Any) -> None:
     data = {
         "a": [False, False, True],
         "b": [False, True, True],
     }
-    df = nw.from_native(constructor(data), eager_only=True)
-    result = df.select(any=nw.any_horizontal(col_expr, nw.col("b")))
+    df = nw.from_native(constructor_lazy(data))
+    result = df.select(any=nw.any_horizontal(nw.col("a"), nw.col("b")))
 
     expected = {"any": [False, True, True]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/arg_true_test.py b/tests/expr_and_series/arg_true_test.py
index 11d43da9..bddb6fb1 100644
--- a/tests/expr_and_series/arg_true_test.py
+++ b/tests/expr_and_series/arg_true_test.py
@@ -4,11 +4,16 @@ import narwhals as nw
 from tests.utils import compare_dicts
 
 
-def test_arg_true(constructor: Any) -> None:
-    df = nw.from_native(constructor({"a": [1, None, None, 3]}), eager_only=True)
+def test_arg_true(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy({"a": [1, None, None, 3]}))
     result = df.select(nw.col("a").is_null().arg_true())
     expected = {"a": [1, 2]}
     compare_dicts(result, expected)
+
+
+def test_arg_true_series(constructor: Any) -> None:
+    df = nw.from_native(constructor({"a": [1, None, None, 3]}), eager_only=True)
     result = df.select(df["a"].is_null().arg_true())
+    expected = {"a": [1, 2]}
     compare_dicts(result, expected)
     assert "a" in df  # cheeky test to hit `__contains__` method
diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py
index f35a45f6..9371c869 100644
--- a/tests/expr_and_series/arithmetic_test.py
+++ b/tests/expr_and_series/arithmetic_test.py
@@ -15,23 +15,22 @@ from tests.utils import compare_dicts
         ("__sub__", 1, [0, 1, 2]),
         ("__mul__", 2, [2, 4, 6]),
         ("__truediv__", 2.0, [0.5, 1.0, 1.5]),
+        ("__truediv__", 1, [1, 2, 3]),
         ("__floordiv__", 2, [0, 1, 1]),
         ("__mod__", 2, [1, 0, 1]),
         ("__pow__", 2, [1, 4, 9]),
     ],
 )
 def test_arithmetic(
-    attr: str, rhs: Any, expected: list[Any], constructor: Any, request: Any
+    attr: str, rhs: Any, expected: list[Any], constructor_lazy: Any, request: Any
 ) -> None:
-    if "pandas_pyarrow" in str(constructor) and attr == "__mod__":
-        request.applymarker(pytest.mark.xfail)
-
-    # pyarrow case
-    if "pyarrow_table" in str(constructor) and attr == "__mod__":
+    if attr == "__mod__" and any(
+        x in str(constructor_lazy) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]
+    ):
         request.applymarker(pytest.mark.xfail)
 
-    data = {"a": [1, 2, 3]}
-    df = nw.from_native(constructor(data))
+    data = {"a": [1.0, 2, 3]}
+    df = nw.from_native(constructor_lazy(data))
     result = df.select(getattr(nw.col("a"), attr)(rhs))
     compare_dicts(result, {"a": expected})
 
@@ -49,18 +48,67 @@ def test_arithmetic(
     ],
 )
 def test_right_arithmetic(
-    attr: str, rhs: Any, expected: list[Any], constructor: Any, request: Any
+    attr: str, rhs: Any, expected: list[Any], constructor_lazy: Any, request: Any
 ) -> None:
-    if "pandas_pyarrow" in str(constructor) and attr in {"__rmod__"}:
+    if attr == "__rmod__" and any(
+        x in str(constructor_lazy) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]
+    ):
         request.applymarker(pytest.mark.xfail)
 
-    # pyarrow case
-    if "table" in str(constructor) and attr in {"__rmod__"}:
+    data = {"a": [1, 2, 3]}
+    df = nw.from_native(constructor_lazy(data))
+    result = df.select(a=getattr(nw.col("a"), attr)(rhs))
+    compare_dicts(result, {"a": expected})
+
+
+@pytest.mark.parametrize(
+    ("attr", "rhs", "expected"),
+    [
+        ("__add__", 1, [2, 3, 4]),
+        ("__sub__", 1, [0, 1, 2]),
+        ("__mul__", 2, [2, 4, 6]),
+        ("__truediv__", 2.0, [0.5, 1.0, 1.5]),
+        ("__truediv__", 1, [1, 2, 3]),
+        ("__floordiv__", 2, [0, 1, 1]),
+        ("__mod__", 2, [1, 0, 1]),
+        ("__pow__", 2, [1, 4, 9]),
+    ],
+)
+def test_arithmetic_series(
+    attr: str, rhs: Any, expected: list[Any], constructor: Any, request: Any
+) -> None:
+    if attr == "__mod__" and any(
+        x in str(constructor) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]
+    ):
         request.applymarker(pytest.mark.xfail)
 
     data = {"a": [1, 2, 3]}
-    df = nw.from_native(constructor(data))
-    result = df.select(a=getattr(nw.col("a"), attr)(rhs))
+    df = nw.from_native(constructor(data), eager_only=True)
+    result = df.select(getattr(df["a"], attr)(rhs))
     compare_dicts(result, {"a": expected})
+
+
+@pytest.mark.parametrize(
+    ("attr", "rhs", "expected"),
+    [
+        ("__radd__", 1, [2, 3, 4]),
+        ("__rsub__", 1, [0, -1, -2]),
+        ("__rmul__", 2, [2, 4, 6]),
+        ("__rtruediv__", 2.0, [2, 1, 2 / 3]),
+        ("__rfloordiv__", 2, [2, 1, 0]),
+        ("__rmod__", 2, [0, 0, 2]),
+        ("__rpow__", 2, [2, 4, 8]),
+    ],
+)
+def test_right_arithmetic_series(
+    attr: str, rhs: Any, expected: list[Any], constructor: Any, request: Any
+) -> None:
+    if attr == "__rmod__" and any(
+        x in str(constructor) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]
+    ):
+        request.applymarker(pytest.mark.xfail)
+
+    data = {"a": [1, 2, 3]}
+    df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(a=getattr(df["a"], attr)(rhs))
     compare_dicts(result, {"a": expected})
diff --git a/tests/expr_and_series/cast_test.py b/tests/expr_and_series/cast_test.py
index 5b16728c..7514808d 100644
--- a/tests/expr_and_series/cast_test.py
+++ b/tests/expr_and_series/cast_test.py
@@ -6,50 +6,54 @@ import pytest
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
 
+data = {
+    "a": [1],
+    "b": [1],
+    "c": [1],
+    "d": [1],
+    "e": [1],
+    "f": [1],
+    "g": [1],
+    "h": [1],
+    "i": [1],
+    "j": [1],
+    "k": ["1"],
+    "l": [1],
+    "m": [True],
+    "n": [True],
+    "o": ["a"],
+    "p": [1],
+}
+schema = {
+    "a": nw.Int64,
+    "b": nw.Int32,
+    "c": nw.Int16,
+    "d": nw.Int8,
+    "e": nw.UInt64,
+    "f": nw.UInt32,
+    "g": nw.UInt16,
+    "h": nw.UInt8,
+    "i": nw.Float64,
+    "j": nw.Float32,
+    "k": nw.String,
+    "l": nw.Datetime,
+    "m": nw.Boolean,
+    "n": nw.Boolean,
+    "o": nw.Categorical,
+    "p": nw.Int64,
+}
+
 
 @pytest.mark.filterwarnings("ignore:casting period[M] values to int64:FutureWarning")
-def test_cast(constructor: Any, request: Any) -> None:
-    if "pyarrow_table_constructor" in str(constructor) and parse_version(
+def test_cast(constructor_lazy: Any, request: Any) -> None:
+    if "pyarrow_table_constructor" in str(constructor_lazy) and parse_version(
         pa.__version__
     ) <= (15,):  # pragma: no cover
         request.applymarker(pytest.mark.xfail)
-    data = {
-        "a": [1],
-        "b": [1],
-        "c": [1],
-        "d": [1],
-        "e": [1],
-        "f": [1],
-        "g": [1],
-        "h": [1],
-        "i": [1],
-        "j": [1],
-        "k": ["1"],
-        "l": [1],
-        "m": [True],
-        "n": [True],
-        "o": ["a"],
-        "p": [1],
-    }
-    schema = {
-        "a": nw.Int64,
-        "b": nw.Int32,
-        "c": nw.Int16,
-        "d": nw.Int8,
-        "e": nw.UInt64,
-        "f": nw.UInt32,
-        "g": nw.UInt16,
-        "h": nw.UInt8,
-        "i": nw.Float64,
-        "j": nw.Float32,
-        "k": nw.String,
-        "l": nw.Datetime,
-        "m": nw.Boolean,
-        "n": nw.Boolean,
-        "o": nw.Categorical,
-        "p": nw.Int64,
-    }
-    df = nw.from_native(constructor(data), eager_only=True).select(
+    if "modin" in str(constructor_lazy):
+        # TODO(unassigned): in modin, we end up with `'<U0'` dtype
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor_lazy(data)).select(
         nw.col(key).cast(value) for key, value in schema.items()
     )
     result = df.select(
@@ -88,7 +92,38 @@ def test_cast(constructor: Any, request: Any) -> None:
         "o": nw.String,
         "p": nw.Duration,
     }
-    assert result.schema == expected
+    assert dict(result.collect_schema()) == expected
+
+
+def test_cast_series(constructor: Any, request: Any) -> None:
+    if "pyarrow_table_constructor" in str(constructor) and parse_version(
+        pa.__version__
+    ) <= (15,):  # pragma: no cover
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # TODO(unassigned): in modin, we end up with `'<U0'` dtype
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor(data), eager_only=True).select(
+        nw.col(key).cast(value) for key, value in schema.items()
+    )
+    expected = {
+        "a": nw.Int32,
+        "b": nw.Int16,
+        "c": nw.Int8,
+        "d": nw.Int64,
+        "e": nw.UInt32,
+        "f": nw.UInt16,
+        "g": nw.UInt8,
+        "h": nw.UInt64,
+        "i": nw.Float32,
+        "j": nw.Float64,
+        "k": nw.String,
+        "l": nw.Datetime,
+        "m": nw.Int8,
+        "n": nw.Int8,
+        "o": nw.String,
+        "p": nw.Duration,
+    }
     result = df.select(
         df["a"].cast(nw.Int32),
         df["b"].cast(nw.Int16),
@@ -107,3 +142,4 @@ def test_cast(constructor: Any, request: Any) -> None:
         df["o"].cast(nw.String),
         df["p"].cast(nw.Duration),
     )
+    assert result.schema == expected
diff --git a/tests/expr_and_series/count_test.py b/tests/expr_and_series/count_test.py
index cd183410..791c34ee 100644
--- a/tests/expr_and_series/count_test.py
+++ b/tests/expr_and_series/count_test.py
@@ -4,9 +4,9 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_count(constructor: Any) -> None:
+def test_count(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, None, 6], "z": [7.0, None, None]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
     result = df.select(nw.col("a", "b", "z").count())
     expected = {"a": [3], "b": [2], "z": [1]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py
index 9d1f8427..e148cb57 100644
--- a/tests/expr_and_series/cum_sum_test.py
+++ b/tests/expr_and_series/cum_sum_test.py
@@ -10,8 +10,8 @@ data = {
 }
 
 
-def test_cum_sum_simple(constructor: Any) -> None:
-    df = nw.from_native(constructor(data), eager_only=True)
+def test_cum_sum_simple(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select(nw.all().cum_sum())
     expected = {
         "a": [0, 1, 3, 6, 10],
@@ -19,6 +19,15 @@ def test_cum_sum_simple(constructor: Any) -> None:
         "c": [5, 9, 12, 14, 15],
     }
     compare_dicts(result, expected)
+
+
+def test_cum_sum_simple_series(constructor: Any) -> None:
+    df = nw.from_native(constructor(data), eager_only=True)
+    expected = {
+        "a": [0, 1, 3, 6, 10],
+        "b": [1, 3, 6, 11, 14],
+        "c": [5, 9, 12, 14, 15],
+    }
     result = df.select(
         df["a"].cum_sum(),
         df["b"].cum_sum(),
diff --git a/tests/expr_and_series/diff_test.py b/tests/expr_and_series/diff_test.py
index 0f79d97f..e4b7e39a 100644
--- a/tests/expr_and_series/diff_test.py
+++ b/tests/expr_and_series/diff_test.py
@@ -14,20 +14,35 @@ data = {
 }
 
 
-def test_diff(constructor: Any, request: Any) -> None:
+def test_diff(constructor_lazy: Any, request: Any) -> None:
+    if "pyarrow_table_constructor" in str(constructor_lazy) and parse_version(
+        pa.__version__
+    ) < (13,):
+        # pc.pairwisediff is available since pyarrow 13.0.0
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor_lazy(data))
+    result = df.with_columns(c_diff=nw.col("c").diff()).filter(nw.col("i") > 0)
+    expected = {
+        "i": [1, 2, 3, 4],
+        "b": [2, 3, 5, 3],
+        "c": [4, 3, 2, 1],
+        "c_diff": [-1, -1, -1, -1],
+    }
+    compare_dicts(result, expected)
+
+
+def test_diff_series(constructor: Any, request: Any) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) < (13,):
         # pc.pairwisediff is available since pyarrow 13.0.0
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data), eager_only=True)
-    result = df.with_columns(c_diff=nw.col("c").diff())[1:]
     expected = {
         "i": [1, 2, 3, 4],
         "b": [2, 3, 5, 3],
         "c": [4, 3, 2, 1],
         "c_diff": [-1, -1, -1, -1],
     }
-    compare_dicts(result, expected)
     result = df.with_columns(c_diff=df["c"].diff())[1:]
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/double_selected_test.py b/tests/expr_and_series/double_selected_test.py
index 2671e7ff..f95c670b 100644
--- a/tests/expr_and_series/double_selected_test.py
+++ b/tests/expr_and_series/double_selected_test.py
@@ -4,9 +4,9 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_double_selected(constructor: Any) -> None:
+def test_double_selected(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
 
     result = df.select(nw.col("a", "b") * 2)
     expected = {"a": [2, 6, 4], "b": [8, 8, 12]}
diff --git a/tests/expr_and_series/double_test.py b/tests/expr_and_series/double_test.py
index d59a64db..d30ceb81 100644
--- a/tests/expr_and_series/double_test.py
+++ b/tests/expr_and_series/double_test.py
@@ -4,17 +4,17 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_double(constructor: Any) -> None:
+def test_double(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
     result = df.with_columns(nw.all() * 2)
     expected = {"a": [2, 6, 4], "b": [8, 8, 12], "z": [14.0, 16.0, 18.0]}
     compare_dicts(result, expected)
 
 
-def test_double_alias(constructor: Any) -> None:
+def test_double_alias(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
     result = df.with_columns(nw.col("a").alias("o"), nw.all() * 2)
     expected = {"o": [1, 3, 2], "a": [2, 6, 4], "b": [8, 8, 12], "z": [14.0, 16.0, 18.0]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/drop_nulls_test.py b/tests/expr_and_series/drop_nulls_test.py
index ef109e01..96f5e3f6 100644
--- a/tests/expr_and_series/drop_nulls_test.py
+++ b/tests/expr_and_series/drop_nulls_test.py
@@ -6,7 +6,7 @@ import narwhals as nw
 from tests.utils import compare_dicts
 
 
-def test_drop_nulls(constructor: Any) -> None:
+def test_drop_nulls(constructor_lazy: Any) -> None:
     data = {
         "A": [1, 2, None, 4],
         "B": [5, 6, 7, 8],
@@ -14,10 +14,10 @@ def test_drop_nulls(constructor: Any) -> None:
         "D": [9, 10, 11, 12],
     }
 
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
 
     result_a = df.select(nw.col("A").drop_nulls())
-    result_b = df.select(df["B"].drop_nulls())
+    result_b = df.select(nw.col("B").drop_nulls())
     result_c = df.select(nw.col("C").drop_nulls())
     result_d = df.select(nw.col("D").drop_nulls())
     expected_a = {"A": [1.0, 2.0, 4.0]}
@@ -29,3 +29,28 @@ def test_drop_nulls(constructor: Any) -> None:
     compare_dicts(result_b, expected_b)
     compare_dicts(result_c, expected_c)
     compare_dicts(result_d, expected_d)
+
+
+def test_drop_nulls_series(constructor: Any) -> None:
+    data = {
+        "A": [1, 2, None, 4],
+        "B": [5, 6, 7, 8],
+        "C": [None, None, None, None],
+        "D": [9, 10, 11, 12],
+    }
+
+    df = nw.from_native(constructor(data), eager_only=True)
+
+    result_a = df.select(df["A"].drop_nulls())
+    result_b = df.select(df["B"].drop_nulls())
+    result_c = df.select(df["C"].drop_nulls())
+    result_d = df.select(df["D"].drop_nulls())
+    expected_a = {"A": [1.0, 2.0, 4.0]}
+    expected_b = {"B": [5, 6, 7, 8]}
+    expected_c = {"C": []}  # type: ignore[var-annotated]
+    expected_d = {"D": [9, 10, 11, 12]}
+
+    compare_dicts(result_a, expected_a)
+    compare_dicts(result_b, expected_b)
+    compare_dicts(result_c, expected_c)
+    compare_dicts(result_d, expected_d)
diff --git a/tests/expr_and_series/dt/to_string_test.py b/tests/expr_and_series/dt/to_string_test.py
index 8fc71e29..c56f6aa4 100644
--- a/tests/expr_and_series/dt/to_string_test.py
+++ b/tests/expr_and_series/dt/to_string_test.py
@@ -27,13 +27,13 @@ def test_dt_to_string(constructor: Any, fmt: str) -> None:
     expected_col = [datetime.strftime(d, fmt) for d in data["a"]]
 
     result = input_series.dt.to_string(fmt).to_list()
-    if "pandas_pyarrow" in str(constructor) or "pyarrow_table" in str(constructor):
+    if any(x in str(constructor) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]):
         # PyArrow differs from other libraries, in that %S also shows
         # the fraction of a second.
         result = [x[: x.find(".")] if "." in x else x for x in result]
     assert result == expected_col
     result = input_frame.select(nw.col("a").dt.to_string(fmt))["a"].to_list()
-    if "pandas_pyarrow" in str(constructor) or "pyarrow_table" in str(constructor):
+    if any(x in str(constructor) for x in ["pandas_pyarrow", "pyarrow_table", "modin"]):
         # PyArrow differs from other libraries, in that %S also shows
         # the fraction of a second.
         result = [x[: x.find(".")] if "." in x else x for x in result]
diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py
index dc9cc738..cc48d223 100644
--- a/tests/expr_and_series/fill_null_test.py
+++ b/tests/expr_and_series/fill_null_test.py
@@ -10,8 +10,8 @@ data = {
 }
 
 
-def test_fill_null(constructor: Any) -> None:
-    df = nw.from_native(constructor(data), eager_only=True)
+def test_fill_null(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
 
     result = df.with_columns(nw.col("a", "b", "c").fill_null(99))
     expected = {
@@ -21,6 +21,15 @@ def test_fill_null(constructor: Any) -> None:
     }
     compare_dicts(result, expected)
 
+
+def test_fill_null_series(constructor: Any) -> None:
+    df = nw.from_native(constructor(data), eager_only=True)
+
+    expected = {
+        "a": [0.0, 99, 2, 3, 4],
+        "b": [1.0, 99, 99, 5, 3],
+        "c": [5.0, 99, 3, 2, 1],
+    }
     result = df.with_columns(
         a=df["a"].fill_null(99),
         b=df["b"].fill_null(99),
diff --git a/tests/expr_and_series/filter_test.py b/tests/expr_and_series/filter_test.py
index ec5d974e..de4de400 100644
--- a/tests/expr_and_series/filter_test.py
+++ b/tests/expr_and_series/filter_test.py
@@ -11,13 +11,15 @@ data = {
 }
 
 
-def test_filter(constructor: Any) -> None:
-    df = nw.from_native(constructor(data), eager_only=True)
+def test_filter(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select(nw.col("a").filter(nw.col("i") < 2, nw.col("c") == 5))
-    expected = {
-        "a": [0],
-    }
+    expected = {"a": [0]}
     compare_dicts(result, expected)
 
+
+def test_filter_series(constructor: Any) -> None:
+    df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(df["a"].filter((df["i"] < 2) & (df["c"] == 5)))
+    expected = {"a": [0]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/gather_every_test.py b/tests/expr_and_series/gather_every_test.py
index 413e63ae..b5e03774 100644
--- a/tests/expr_and_series/gather_every_test.py
+++ b/tests/expr_and_series/gather_every_test.py
@@ -10,8 +10,8 @@ data = {"a": list(range(10))}
 
 @pytest.mark.parametrize("n", [1, 2, 3])
 @pytest.mark.parametrize("offset", [1, 2, 3])
-def test_gather_every_expr(constructor_with_lazy: Any, n: int, offset: int) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_gather_every_expr(constructor_lazy: Any, n: int, offset: int) -> None:
+    df = nw.from_native(constructor_lazy(data))
 
     result = df.select(nw.col("a").gather_every(n=n, offset=offset))
     expected = {"a": data["a"][offset::n]}
diff --git a/tests/expr_and_series/is_duplicated_test.py b/tests/expr_and_series/is_duplicated_test.py
index 4b2e0d51..ff69e4bf 100644
--- a/tests/expr_and_series/is_duplicated_test.py
+++ b/tests/expr_and_series/is_duplicated_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import numpy as np
+import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -11,7 +12,10 @@ data = {
 }
 
 
-def test_is_duplicated_expr(constructor: Any) -> None:
+def test_is_duplicated_expr(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        # TODO(unassigned): why is Modin failing here?
+        request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.all().is_duplicated())
     expected = {
diff --git a/tests/expr_and_series/is_first_distinct_test.py b/tests/expr_and_series/is_first_distinct_test.py
index f30ed92c..d792f0ac 100644
--- a/tests/expr_and_series/is_first_distinct_test.py
+++ b/tests/expr_and_series/is_first_distinct_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import numpy as np
+import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -11,7 +12,10 @@ data = {
 }
 
 
-def test_is_first_distinct_expr(constructor: Any) -> None:
+def test_is_first_distinct_expr(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        # TODO(unassigned): why is Modin failing here?
+        request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.all().is_first_distinct())
     expected = {
diff --git a/tests/expr_and_series/is_last_distinct_test.py b/tests/expr_and_series/is_last_distinct_test.py
index 78f8baaa..38f981d0 100644
--- a/tests/expr_and_series/is_last_distinct_test.py
+++ b/tests/expr_and_series/is_last_distinct_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import numpy as np
+import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -11,7 +12,10 @@ data = {
 }
 
 
-def test_is_last_distinct_expr(constructor: Any) -> None:
+def test_is_last_distinct_expr(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        # TODO(unassigned): why is Modin failing here?
+        request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.all().is_last_distinct())
     expected = {
diff --git a/tests/expr_and_series/is_unique_test.py b/tests/expr_and_series/is_unique_test.py
index b8c49855..9af0f3d9 100644
--- a/tests/expr_and_series/is_unique_test.py
+++ b/tests/expr_and_series/is_unique_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import numpy as np
+import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -11,7 +12,10 @@ data = {
 }
 
 
-def test_is_unique_expr(constructor: Any) -> None:
+def test_is_unique_expr(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        # TODO(unassigned): why is Modin failing here?
+        request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.all().is_unique())
     expected = {
diff --git a/tests/expr_and_series/name/keep_test.py b/tests/expr_and_series/name/keep_test.py
index 413a7c7e..78b4aa29 100644
--- a/tests/expr_and_series/name/keep_test.py
+++ b/tests/expr_and_series/name/keep_test.py
@@ -12,15 +12,15 @@ from tests.utils import compare_dicts
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_keep(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_keep(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.keep())
     expected = {k: [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_keep_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_keep_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.keep())
     expected = {"foo": data["foo"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/name/map_test.py b/tests/expr_and_series/name/map_test.py
index 4460a377..b86c8b8f 100644
--- a/tests/expr_and_series/name/map_test.py
+++ b/tests/expr_and_series/name/map_test.py
@@ -16,15 +16,15 @@ def map_func(s: str | None) -> str:
     return str(s)[::-1].lower()
 
 
-def test_map(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_map(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.map(function=map_func))
     expected = {map_func(k): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_map_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_map_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.map(function=map_func))
     expected = {map_func("foo"): data["foo"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/name/prefix_test.py b/tests/expr_and_series/name/prefix_test.py
index 3b84f689..ba0f85a1 100644
--- a/tests/expr_and_series/name/prefix_test.py
+++ b/tests/expr_and_series/name/prefix_test.py
@@ -13,15 +13,15 @@ data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 prefix = "with_prefix_"
 
 
-def test_prefix(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_prefix(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.prefix(prefix))
     expected = {prefix + str(k): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_suffix_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_suffix_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.prefix(prefix))
     expected = {prefix + "foo": data["foo"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/name/suffix_test.py b/tests/expr_and_series/name/suffix_test.py
index 2f1abce5..93e3887d 100644
--- a/tests/expr_and_series/name/suffix_test.py
+++ b/tests/expr_and_series/name/suffix_test.py
@@ -13,15 +13,15 @@ data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 suffix = "_with_suffix"
 
 
-def test_suffix(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_suffix(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.suffix(suffix))
     expected = {str(k) + suffix: [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_suffix_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_suffix_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.suffix(suffix))
     expected = {"foo" + suffix: data["foo"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/name/to_lowercase_test.py b/tests/expr_and_series/name/to_lowercase_test.py
index 4d190c2f..d66358e4 100644
--- a/tests/expr_and_series/name/to_lowercase_test.py
+++ b/tests/expr_and_series/name/to_lowercase_test.py
@@ -12,15 +12,15 @@ from tests.utils import compare_dicts
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_to_lowercase(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_to_lowercase(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.to_lowercase())
     expected = {k.lower(): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_to_lowercase_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_to_lowercase_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("BAR")).alias("ALIAS_FOR_BAR").name.to_lowercase())
     expected = {"bar": data["BAR"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/name/to_uppercase_test.py b/tests/expr_and_series/name/to_uppercase_test.py
index 2b1342ec..00ad506a 100644
--- a/tests/expr_and_series/name/to_uppercase_test.py
+++ b/tests/expr_and_series/name/to_uppercase_test.py
@@ -12,15 +12,15 @@ from tests.utils import compare_dicts
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_to_uppercase(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_to_uppercase(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.to_uppercase())
     expected = {k.upper(): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_to_uppercase_after_alias(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_to_uppercase_after_alias(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.to_uppercase())
     expected = {"FOO": data["foo"]}
     compare_dicts(result, expected)
diff --git a/tests/expr_and_series/str/starts_with_ends_with_test.py b/tests/expr_and_series/str/starts_with_ends_with_test.py
index 9216f2e6..a83b6baa 100644
--- a/tests/expr_and_series/str/starts_with_ends_with_test.py
+++ b/tests/expr_and_series/str/starts_with_ends_with_test.py
@@ -11,8 +11,8 @@ from tests.utils import compare_dicts
 data = {"a": ["fdas", "edfas"]}
 
 
-def test_ends_with(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data)).lazy()
+def test_ends_with(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data)).lazy()
     result = df.select(nw.col("a").str.ends_with("das"))
     expected = {
         "a": [True, False],
@@ -26,8 +26,8 @@ def test_ends_with(constructor_with_lazy: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_starts_with(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data)).lazy()
+def test_starts_with(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data)).lazy()
     result = df.select(nw.col("a").str.starts_with("fda"))
     expected = {
         "a": [True, False],
diff --git a/tests/expr_and_series/sum_horizontal_test.py b/tests/expr_and_series/sum_horizontal_test.py
index 524d3f87..b6b9a612 100644
--- a/tests/expr_and_series/sum_horizontal_test.py
+++ b/tests/expr_and_series/sum_horizontal_test.py
@@ -7,9 +7,9 @@ from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
-def test_sumh(constructor: Any, col_expr: Any) -> None:
+def test_sumh(constructor_lazy: Any, col_expr: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_lazy(data))
     result = df.with_columns(horizontal_sum=nw.sum_horizontal(col_expr, nw.col("b")))
     expected = {
         "a": [1, 3, 2],
diff --git a/tests/expr_and_series/unary_test.py b/tests/expr_and_series/unary_test.py
index 0d9b6874..caceea1c 100644
--- a/tests/expr_and_series/unary_test.py
+++ b/tests/expr_and_series/unary_test.py
@@ -4,10 +4,10 @@ import narwhals as nw
 from tests.utils import compare_dicts
 
 
-def test_unary(constructor_with_lazy: Any) -> None:
+def test_unary(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     result = (
-        nw.from_native(constructor_with_lazy(data))
+        nw.from_native(constructor_lazy(data))
         .with_columns(
             a_mean=nw.col("a").mean(),
             a_sum=nw.col("a").sum(),
@@ -19,3 +19,17 @@ def test_unary(constructor_with_lazy: Any) -> None:
     )
     expected = {"a_mean": [2], "a_sum": [6], "b_nunique": [2], "z_min": [7], "z_max": [9]}
     compare_dicts(result, expected)
+
+
+def test_unary_series(constructor: Any) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    df = nw.from_native(constructor(data), eager_only=True)
+    result = {
+        "a_mean": [df["a"].mean()],
+        "a_sum": [df["a"].sum()],
+        "b_nunique": [df["b"].n_unique()],
+        "z_min": [df["z"].min()],
+        "z_max": [df["z"].max()],
+    }
+    expected = {"a_mean": [2], "a_sum": [6], "b_nunique": [2], "z_min": [7], "z_max": [9]}
+    compare_dicts(result, expected)
diff --git a/tests/expr_and_series/unique_test.py b/tests/expr_and_series/unique_test.py
index cc06cc6a..1946fca9 100644
--- a/tests/expr_and_series/unique_test.py
+++ b/tests/expr_and_series/unique_test.py
@@ -8,8 +8,8 @@ from tests.utils import compare_dicts
 data = {"a": [1, 1, 2]}
 
 
-def test_unique_expr(constructor: Any) -> None:
-    df = nw.from_native(constructor(data), eager_only=True)
+def test_unique_expr(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.select(nw.col("a").unique())
     expected = {"a": [1, 2]}
     compare_dicts(result, expected)
diff --git a/tests/frame/clone_test.py b/tests/frame/clone_test.py
index 0b26df19..539d9029 100644
--- a/tests/frame/clone_test.py
+++ b/tests/frame/clone_test.py
@@ -6,12 +6,12 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_clone(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
+def test_clone(request: Any, constructor_lazy: Any) -> None:
+    if "pyarrow_table" in str(constructor_lazy):
         request.applymarker(pytest.mark.xfail)
 
     expected = {"a": [1, 2], "b": [3, 4]}
-    df = nw.from_native(constructor_with_lazy(expected))
+    df = nw.from_native(constructor_lazy(expected))
     df_clone = df.clone()
     assert df is not df_clone
     assert df._compliant_frame is not df_clone._compliant_frame
diff --git a/tests/frame/concat_test.py b/tests/frame/concat_test.py
index e52b33e8..b3217ac4 100644
--- a/tests/frame/concat_test.py
+++ b/tests/frame/concat_test.py
@@ -6,12 +6,12 @@ import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 
-def test_concat_horizontal(constructor_with_lazy: Any) -> None:
+def test_concat_horizontal(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df_left = nw.from_native(constructor_with_lazy(data))
+    df_left = nw.from_native(constructor_lazy(data))
 
     data_right = {"c": [6, 12, -1], "d": [0, -4, 2]}
-    df_right = nw.from_native(constructor_with_lazy(data_right))
+    df_right = nw.from_native(constructor_lazy(data_right))
 
     result = nw.concat([df_left, df_right], how="horizontal")
     expected = {
@@ -27,17 +27,17 @@ def test_concat_horizontal(constructor_with_lazy: Any) -> None:
         nw.concat([])
 
 
-def test_concat_vertical(constructor_with_lazy: Any) -> None:
+def test_concat_vertical(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_left = (
-        nw.from_native(constructor_with_lazy(data))
+        nw.from_native(constructor_lazy(data))
         .rename({"a": "c", "b": "d"})
         .drop("z")
         .lazy()
     )
 
     data_right = {"c": [6, 12, -1], "d": [0, -4, 2]}
-    df_right = nw.from_native(constructor_with_lazy(data_right)).lazy()
+    df_right = nw.from_native(constructor_lazy(data_right)).lazy()
 
     result = nw.concat([df_left, df_right], how="vertical")
     expected = {"c": [1, 3, 2, 6, 12, -1], "d": [4, 4, 6, 0, -4, 2]}
diff --git a/tests/frame/gather_every_test.py b/tests/frame/gather_every_test.py
index 7dffa675..713c977b 100644
--- a/tests/frame/gather_every_test.py
+++ b/tests/frame/gather_every_test.py
@@ -10,8 +10,8 @@ data = {"a": list(range(10))}
 
 @pytest.mark.parametrize("n", [1, 2, 3])
 @pytest.mark.parametrize("offset", [1, 2, 3])
-def test_gather_every(constructor_with_lazy: Any, n: int, offset: int) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_gather_every(constructor_lazy: Any, n: int, offset: int) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.gather_every(n=n, offset=offset)
     expected = {"a": data["a"][offset::n]}
     compare_dicts(result, expected)
diff --git a/tests/frame/lit_test.py b/tests/frame/lit_test.py
index a15f7411..f9099558 100644
--- a/tests/frame/lit_test.py
+++ b/tests/frame/lit_test.py
@@ -17,11 +17,9 @@ if TYPE_CHECKING:
     ("dtype", "expected_lit"),
     [(None, [2, 2, 2]), (nw.String, ["2", "2", "2"]), (nw.Float32, [2.0, 2.0, 2.0])],
 )
-def test_lit(
-    constructor_with_lazy: Any, dtype: DType | None, expected_lit: list[Any]
-) -> None:
+def test_lit(constructor_lazy: Any, dtype: DType | None, expected_lit: list[Any]) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df_raw = constructor_with_lazy(data)
+    df_raw = constructor_lazy(data)
     df = nw.from_native(df_raw).lazy()
     result = df.with_columns(nw.lit(2, dtype).alias("lit"))
     expected = {
@@ -33,9 +31,9 @@ def test_lit(
     compare_dicts(result, expected)
 
 
-def test_lit_error(constructor_with_lazy: Any) -> None:
+def test_lit_error(constructor_lazy: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df_raw = constructor_with_lazy(data)
+    df_raw = constructor_lazy(data)
     df = nw.from_native(df_raw).lazy()
     with pytest.raises(
         ValueError, match="numpy arrays are not supported as literal values"
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index 667e0279..af0a2775 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -39,9 +39,9 @@ def test_std(constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore:Determining|Resolving.*")
-def test_schema(constructor_with_lazy: Any) -> None:
+def test_schema(constructor_lazy: Any) -> None:
     df = nw.from_native(
-        constructor_with_lazy({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]})
+        constructor_lazy({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]})
     )
     result = df.schema
     expected = {"a": nw.Int64, "b": nw.Int64, "z": nw.Float64}
@@ -52,9 +52,9 @@ def test_schema(constructor_with_lazy: Any) -> None:
     assert result == expected
 
 
-def test_collect_schema(constructor_with_lazy: Any) -> None:
+def test_collect_schema(constructor_lazy: Any) -> None:
     df = nw.from_native(
-        constructor_with_lazy({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]})
+        constructor_lazy({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]})
     )
     expected = {"a": nw.Int64, "b": nw.Int64, "z": nw.Float64}
 
@@ -65,18 +65,14 @@ def test_collect_schema(constructor_with_lazy: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore:Determining|Resolving.*")
-def test_columns(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data))
+def test_columns(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_lazy(data))
     result = df.columns
     expected = ["a", "b", "z"]
     assert result == expected
 
 
-def test_expr_binary(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor) or "pandas_pyarrow" in str(
-        constructor
-    ):  # pandas pyarrow raises NotImplementedError for __mod__
-        request.applymarker(pytest.mark.xfail)
+def test_expr_binary(constructor: Any) -> None:
     df_raw = constructor(data)
     result = nw.from_native(df_raw).with_columns(
         a=(1 + 3 * nw.col("a")) * (1 / nw.col("a")),
@@ -96,10 +92,7 @@ def test_expr_binary(request: Any, constructor: Any) -> None:
         k=2 // nw.col("a"),
         l=nw.col("a") // 2,
         m=nw.col("a") ** 2,
-        n=nw.col("a") % 2,
-        o=2 % nw.col("a"),
     )
-    result_native = nw.to_native(result)
     expected = {
         "a": [4, 3.333333, 3.5],
         "b": [-3.5, -4.0, -2.25],
@@ -115,10 +108,8 @@ def test_expr_binary(request: Any, constructor: Any) -> None:
         "k": [2, 0, 1],
         "l": [0, 1, 1],
         "m": [1, 9, 4],
-        "n": [1, 1, 0],
-        "o": [0, 2, 0],
     }
-    compare_dicts(result_native, expected)
+    compare_dicts(result, expected)
 
 
 def test_expr_transform(request: Any, constructor: Any) -> None:
@@ -131,8 +122,8 @@ def test_expr_transform(request: Any, constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_expr_na(constructor_with_lazy: Any) -> None:
-    df = nw.from_native(constructor_with_lazy(data_na)).lazy()
+def test_expr_na(constructor_lazy: Any) -> None:
+    df = nw.from_native(constructor_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]}
     compare_dicts(result_nna, expected)
diff --git a/tests/frame/to_dict_test.py b/tests/frame/to_dict_test.py
index b0950c5c..a9927eaf 100644
--- a/tests/frame/to_dict_test.py
+++ b/tests/frame/to_dict_test.py
@@ -1,8 +1,13 @@
 from typing import Any
 
+import pytest
+
 import narwhals.stable.v1 as nw
 
 
+@pytest.mark.filterwarnings(
+    "ignore:.*all arguments of to_dict except for the argument:FutureWarning"
+)
 def test_to_dict(constructor: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "c": [7.0, 8, 9]}
     df = nw.from_native(constructor(data), eager_only=True)
diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py
index c9483eed..9b628112 100644
--- a/tests/frame/to_pandas_test.py
+++ b/tests/frame/to_pandas_test.py
@@ -14,7 +14,9 @@ from narwhals.utils import parse_version
     parse_version(pd.__version__) < parse_version("2.0.0"),
     reason="too old for pandas-pyarrow",
 )
-def test_convert_pandas(constructor: Any) -> None:
+def test_convert_pandas(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_raw = constructor(data)
     result = nw.from_native(df_raw).to_pandas()  # type: ignore[union-attr]
diff --git a/tests/frame/unique_test.py b/tests/frame/unique_test.py
index 2556358a..9fbaea74 100644
--- a/tests/frame/unique_test.py
+++ b/tests/frame/unique_test.py
@@ -21,20 +21,20 @@ data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     ],
 )
 def test_unique(
-    constructor_with_lazy: Any,
+    constructor_lazy: Any,
     subset: str | list[str] | None,
     keep: str,
     expected: dict[str, list[float]],
 ) -> None:
-    df_raw = constructor_with_lazy(data)
+    df_raw = constructor_lazy(data)
     df = nw.from_native(df_raw)
 
     result = df.unique(subset, keep=keep, maintain_order=True)  # type: ignore[arg-type]
     compare_dicts(result, expected)
 
 
-def test_unique_none(constructor_with_lazy: Any) -> None:
-    df_raw = constructor_with_lazy(data)
+def test_unique_none(constructor_lazy: Any) -> None:
+    df_raw = constructor_lazy(data)
     df = nw.from_native(df_raw)
 
     result = df.unique(maintain_order=True)
diff --git a/tests/series_only/arithmetic_test.py b/tests/series_only/arithmetic_test.py
index 59e16547..71bd81fc 100644
--- a/tests/series_only/arithmetic_test.py
+++ b/tests/series_only/arithmetic_test.py
@@ -15,39 +15,6 @@ from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 
-@pytest.mark.parametrize("data", [[1, 2, 3], [1.0, 2, 3]])
-@pytest.mark.parametrize(
-    ("attr", "rhs", "expected"),
-    [
-        ("__add__", 1, [2, 3, 4]),
-        ("__sub__", 1, [0, 1, 2]),
-        ("__mul__", 2, [2, 4, 6]),
-        ("__truediv__", 2, [0.5, 1.0, 1.5]),
-        ("__truediv__", 1, [1, 2, 3]),
-        ("__floordiv__", 2, [0, 1, 1]),
-        ("__mod__", 2, [1, 0, 1]),
-        ("__pow__", 2, [1, 4, 9]),
-    ],
-)
-def test_arithmetic(
-    request: Any,
-    data: list[int | float],
-    attr: str,
-    rhs: Any,
-    expected: list[Any],
-    constructor: Any,
-) -> None:
-    if "pandas_pyarrow" in str(constructor) and attr == "__mod__":
-        request.applymarker(pytest.mark.xfail)
-
-    if "table" in str(constructor) and attr == "__mod__":
-        request.applymarker(pytest.mark.xfail)
-
-    s = nw.from_native(constructor({"a": data}), eager_only=True)["a"]
-    result = getattr(s, attr)(rhs)
-    assert result.to_numpy().tolist() == expected
-
-
 def test_truediv_same_dims(constructor: Any, request: Any) -> None:
     if "polars" in str(constructor):
         # https://github.com/pola-rs/polars/issues/17760
diff --git a/tests/series_only/test_common.py b/tests/series_only/test_common.py
index 40fb2c70..46255979 100644
--- a/tests/series_only/test_common.py
+++ b/tests/series_only/test_common.py
@@ -91,7 +91,10 @@ def test_boolean_reductions(request: Any, constructor: Any) -> None:
     parse_version(pd.__version__) < parse_version("2.0.0"), reason="too old for pyarrow"
 )
 def test_convert(request: Any, constructor: Any) -> None:
-    if any(cname in str(constructor) for cname in ("pandas_nullable", "pandas_pyarrow")):
+    if any(
+        cname in str(constructor)
+        for cname in ("pandas_nullable", "pandas_pyarrow", "modin")
+    ):
         request.applymarker(pytest.mark.xfail)
 
     series = nw.from_native(constructor({"a": data}), eager_only=True)["a"].alias("a")
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index df3f314f..7d96e7f0 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -156,7 +156,10 @@ def test_group_by_multiple_keys(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_key_with_nulls(constructor: Any) -> None:
+def test_key_with_nulls(constructor: Any, request: Any) -> None:
+    if "modin" in str(constructor):
+        # TODO(unassigned): Modin flaky here?
+        request.applymarker(pytest.mark.skip)
     context = (
         pytest.raises(NotImplementedError, match="null values")
         if (