Repository: narwhals-dev/narwhals
Pull request #1608 (2024-12-17 10:59:40)

ci: Speed up tests in local development
<!--
# 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 #\<issue number\>
- Closes #\<issue number\>

## 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/.github/workflows/check_docs_build.yml b/.github/workflows/check_docs_build.yml
index fd878414..c85fa35d 100644
--- a/.github/workflows/check_docs_build.yml
+++ b/.github/workflows/check_docs_build.yml
@@ -23,13 +23,9 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
-      - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt --system
-      - name: install-docs-reqs
-        run: uv pip install --upgrade -r docs/requirements-docs.txt --system
+          cache-dependency-glob: "pyproject.toml"
       - name: local-install
-        run: uv pip install -e . --system
+        run: uv pip install -e .[docs] --system
       - name: check-no-errors
         run: |
           python -m mkdocs build 2>&1 | tee output.txt
diff --git a/.github/workflows/check_tpch_queries.yml b/.github/workflows/check_tpch_queries.yml
index b727ab56..d98d205b 100644
--- a/.github/workflows/check_tpch_queries.yml
+++ b/.github/workflows/check_tpch_queries.yml
@@ -23,11 +23,9 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
-      - name: install-reqs
-        run: uv pip install --upgrade -r requirements-dev.txt --system
+          cache-dependency-glob: "pyproject.toml"
       - name: local-install
-        run: uv pip install -e . --system
+        run: uv pip install -e ".[dev]" --system
       - name: generate-data
         run: cd tpch && python generate_data.py
       - name: tpch-tests 
diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml
index 166b17fb..b1939004 100644
--- a/.github/workflows/extremes.yml
+++ b/.github/workflows/extremes.yml
@@ -23,12 +23,12 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-minimum-versions
         run: uv pip install tox virtualenv setuptools pandas==0.25.3 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 scipy==1.5.0 scikit-learn==1.1.0 tzdata --system
       - name: install-reqs
         run: |
-          uv pip install -r requirements-dev.txt --system
+          uv pip install -e ".[dev]" --system
           : # pyspark >= 3.3.0 is not compatible with pandas==0.25.3
           uv pip uninstall pyspark --system
       - name: show-deps
@@ -53,11 +53,11 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-minimum-versions
         run: uv pip install tox virtualenv setuptools pandas==1.1.5 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 pyspark==3.3.0 scipy==1.5.0 scikit-learn==1.1.0 tzdata --system
       - name: install-reqs
-        run: uv pip install -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: show-deps
         run: uv pip freeze
       - name: Run pytest
@@ -80,11 +80,11 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-not-so-old-versions
         run: uv pip install tox virtualenv setuptools pandas==2.0.3 polars==0.20.8 numpy==1.24.4 pyarrow==14.0.0 pyspark==3.4.0 scipy==1.8.0 scikit-learn==1.3.0 dask[dataframe]==2024.7 tzdata --system
       - name: install-reqs
-        run: uv pip install -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: show-deps
         run: uv pip freeze
       - name: Run pytest
@@ -107,9 +107,9 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: install-kaggle
         run: uv pip install kaggle --system
       - name: Download Kaggle notebook artifact
diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml
index 354dafd4..8aaaba51 100644
--- a/.github/workflows/mkdocs.yml
+++ b/.github/workflows/mkdocs.yml
@@ -27,6 +27,9 @@ jobs:
           path: .cache
           restore-keys: |
             mkdocs-material-
-      - run: pip install -r docs/requirements-docs.txt -e . pandas polars pyarrow
+      - name: griffe
+        # hopefully temporary until https://github.com/mkdocstrings/mkdocstrings/issues/716
+        run: pip install git+https://github.com/MarcoGorelli/griffe.git@no-overloads
+      - run: pip install -e .[docs]
 
       - run: mkdocs gh-deploy --force
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 86e98ebb..5d728590 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -96,9 +96,13 @@ Here's how you can set up your local development environment to contribute.
       uv venv -p 3.12 --seed
       ```
    4. Activate it. On Linux, this is `. .venv/bin/activate`, on Windows `.\.venv\Scripts\activate`.
-2. Install Narwhals: `uv pip install -e .`
-3. Install test requirements: `uv pip install -r requirements-dev.txt`
-4. Install docs requirements: `uv pip install -r docs/requirements-docs.txt`
+2. Install Narwhals: `uv pip install -e ".[dev, docs]"`
+3. Install a fork of griffe:
+   ```
+   uv pip install git+https://github.com/MarcoGorelli/griffe.git@no-overloads
+   ```
+   This is hopefully temporary until https://github.com/mkdocstrings/mkdocstrings/issues/716
+   is addressed.
 
 You should also install pre-commit:
 ```
@@ -138,6 +142,22 @@ nox
 
 Notice that nox will also require to have all the python versions that are defined in the `noxfile.py` installed in your system.
 
+#### Hypothesis tests
+
+We use Hypothesis to generate some random tests, to check for robustness.
+To keep local test suite times down, not all of these run by default - you can
+run them by passing the `--runslow` flag to PyTest.
+
+#### Testing Dask and Modin
+
+To keep local development test times down, Dask and Modin are excluded from dev
+dependencies, and their tests only run in CI. If you install them with
+
+```
+uv pip install -U dask[dataframe] modin
+```
+then their tests will run too.
+
 #### Testing cuDF
 
 We can't currently test in CI against cuDF, but you can test it manually in Kaggle using GPUs. Please follow this [Kaggle notebook](https://www.kaggle.com/code/marcogorelli/testing-cudf-in-narwhals) to run the tests.
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index ef9c7a4d..96af851e 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -9,6 +9,7 @@ from typing import Sequence
 from narwhals._expression_parsing import reuse_series_implementation
 from narwhals._expression_parsing import reuse_series_namespace_implementation
 from narwhals._pandas_like.series import PandasLikeSeries
+from narwhals._pandas_like.utils import rename
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import is_numpy_array
 from narwhals.exceptions import ColumnNotFoundError
@@ -23,6 +24,17 @@ if TYPE_CHECKING:
     from narwhals.utils import Implementation
     from narwhals.utils import Version
 
+CUMULATIVE_FUNCTIONS_TO_PANDAS_EQUIVALENT = {
+    "col->cum_sum": "cumsum",
+    "col->cum_min": "cummin",
+    "col->cum_max": "cummax",
+    "col->cum_prod": "cumprod",
+    # Pandas cumcount starts counting from 0 while Polars starts from 1
+    # Pandas cumcount counts nulls while Polars does not
+    # So, instead of using "cumcount" we use "cumsum" on notna() to get the same result
+    "col->cum_count": "cumsum",
+}
+
 
 class PandasLikeExpr(CompliantExpr[PandasLikeSeries]):
     def __init__(
@@ -400,19 +412,54 @@ class PandasLikeExpr(CompliantExpr[PandasLikeSeries]):
         )
 
     def over(self, keys: list[str]) -> Self:
-        def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
-            if self._output_names is None:
-                msg = (
-                    "Anonymous expressions are not supported in over.\n"
-                    "Instead of `nw.all()`, try using a named expression, such as "
-                    "`nw.col('a', 'b')`\n"
+        if self._function_name in CUMULATIVE_FUNCTIONS_TO_PANDAS_EQUIVALENT:
+
+            def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
+                if (
+                    self._output_names is None or self._root_names is None
+                ):  # pragma: no cover
+                    # Technically unreachable, but keep this for safety
+                    msg = (
+                        "Anonymous expressions are not supported in over.\n"
+                        "Instead of `nw.all()`, try using a named expression, such as "
+                        "`nw.col('a', 'b')`\n"
+                    )
+                    raise ValueError(msg)
+
+                if self._function_name == "col->cum_count":
+                    plx = self.__narwhals_namespace__()
+                    df = df.with_columns(~plx.col(*self._root_names).is_null())
+
+                res_native = df._native_frame.groupby(list(keys), as_index=False)[
+                    self._root_names
+                ].transform(
+                    CUMULATIVE_FUNCTIONS_TO_PANDAS_EQUIVALENT[self._function_name]
                 )
-                raise ValueError(msg)
-            tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
-            tmp = df.select(*keys).join(
-                tmp, how="left", left_on=keys, right_on=keys, suffix="_right"
-            )
-            return [tmp[name] for name in self._output_names]
+                result_frame = df._from_native_frame(
+                    rename(
+                        res_native,
+                        columns=dict(zip(self._root_names, self._output_names)),
+                        implementation=self._implementation,
+                        backend_version=self._backend_version,
+                    )
+                )
+                return [result_frame[name] for name in self._output_names]
+
+        else:
+
+            def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
+                if self._output_names is None:
+                    msg = (
+                        "Anonymous expressions are not supported in over.\n"
+                        "Instead of `nw.all()`, try using a named expression, such as "
+                        "`nw.col('a', 'b')`\n"
+                    )
+                    raise ValueError(msg)
+                tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
+                tmp = df.select(*keys).join(
+                    tmp, how="left", left_on=keys, right_on=keys, suffix="_right"
+                )
+                return [tmp[name] for name in self._output_names]
 
         return self.__class__(
             func,
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 1bc55baa..41038670 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -2262,7 +2262,7 @@ class Expr:
 
             Let's define a dataframe-agnostic function:
 
-            >>> def my_library_agnostic_function(df_native: IntoFrameT) -> IntoFrameT:
+            >>> def agnostic_min_over_b(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
             ...     return df.with_columns(
             ...         a_min_per_group=nw.col("a").min().over("b")
@@ -2270,12 +2270,12 @@ class Expr:
 
             We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
-            >>> my_library_agnostic_function(df_pd)
+            >>> agnostic_min_over_b(df_pd)
                a  b  a_min_per_group
             0  1  1                1
             1  2  1                1
             2  3  2                3
-            >>> my_library_agnostic_function(df_pl)
+            >>> agnostic_min_over_b(df_pl)
             shape: (3, 3)
             ┌─────┬─────┬─────────────────┐
             │ a   ┆ b   ┆ a_min_per_group │
@@ -2286,7 +2286,7 @@ class Expr:
             │ 2   ┆ 1   ┆ 1               │
             │ 3   ┆ 2   ┆ 3               │
             └─────┴─────┴─────────────────┘
-            >>> my_library_agnostic_function(df_pa)
+            >>> agnostic_min_over_b(df_pa)
             pyarrow.Table
             a: int64
             b: int64
@@ -2295,6 +2295,30 @@ class Expr:
             a: [[1,2,3]]
             b: [[1,1,2]]
             a_min_per_group: [[1,1,3]]
+
+            Cumulative operations are also supported, but (currently) only for
+            pandas and Polars:
+
+            >>> def agnostic_cum_sum(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.with_columns(c=nw.col("a").cum_sum().over("b")).to_native()
+
+            >>> agnostic_cum_sum(df_pd)
+               a  b  c
+            0  1  1  1
+            1  2  1  3
+            2  3  2  3
+            >>> agnostic_cum_sum(df_pl)
+            shape: (3, 3)
+            ┌─────┬─────┬─────┐
+            │ a   ┆ b   ┆ c   │
+            │ --- ┆ --- ┆ --- │
+            │ i64 ┆ i64 ┆ i64 │
+            ╞═════╪═════╪═════╡
+            │ 1   ┆ 1   ┆ 1   │
+            │ 2   ┆ 1   ┆ 3   │
+            │ 3   ┆ 2   ┆ 3   │
+            └─────┴─────┴─────┘
         """
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).over(flatten(keys))
diff --git a/noxfile.py b/noxfile.py
index ccb6bae2..5525c781 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -14,7 +14,7 @@ PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"]
 
 
 def run_common(session: Session, coverage_threshold: float) -> None:
-    session.install("-e.", "-r", "requirements-dev.txt")
+    session.install('-e ".[dev]"')
 
     if session.python != "3.8":
         session.install("ibis-framework>=6.0.0", "rich", "packaging", "pyarrow_hotfix")
diff --git a/pyproject.toml b/pyproject.toml
index cc92d1b7..bc13ae65 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,7 +28,6 @@ exclude = [
   "CONTRIBUTING.md",
   "mkdocs.yml",
   "noxfile.py",
-  "requirements-dev.txt",
 ]
 
 [project.optional-dependencies]
@@ -39,6 +38,38 @@ polars = ["polars>=0.20.3"]
 pyarrow = ["pyarrow>=11.0.0"]
 dask = ["dask[dataframe]>=2024.7"]
 pyspark = ["pyspark>=3.3.0"]
+dev = [
+  "tqdm",
+  "covdefaults",
+  "duckdb",
+  "pandas",
+  "polars",
+  "pyarrow",
+  "pyspark; python_version < '3.12'",
+  "pyarrow-stubs",
+  "pytest",
+  "pytest-cov",
+  "pytest-randomly",
+  "pytest-env",
+  "hypothesis",
+  "hypothesis[numpy]",
+  "scikit-learn",
+  "typing_extensions",
+  "dask[dataframe]; python_version >= '3.9'",
+]
+docs = [
+  "black",  # required by mkdocstrings_handlers
+  "jinja2",
+  "duckdb",
+  "markdown-exec[ansi]",
+  "mkdocs",
+  "mkdocs-autorefs",
+  "mkdocs-material",
+  "mkdocstrings[python]",
+  "pandas",
+  "polars>=1.0.0",
+  "pyarrow",
+]
 
 [project.urls]
 "Homepage" = "https://github.com/narwhals-dev/narwhals"
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index ff842920..00000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-tqdm
-covdefaults
-duckdb
-pandas
-polars
-pre-commit
-pyarrow
-pyspark; python_version < '3.12'
-pyarrow-stubs
-pytest
-pytest-cov
-pytest-randomly
-pytest-env
-hypothesis
-hypothesis[numpy]
-scikit-learn
-typing_extensions
-dask[dataframe]; python_version >= '3.9'


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 954b4214..bcc3a2af 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -23,7 +23,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-altair
         run: |
             git clone https://github.com/vega/altair.git --depth=1
@@ -70,7 +70,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-marimo
         run: |
             git clone https://github.com/marimo-team/marimo.git --depth=1
@@ -131,7 +131,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-scikit-lego
         run: git clone https://github.com/koaning/scikit-lego.git --depth 1
       - name: install-basics
@@ -170,7 +170,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-shiny
         run: |
             git clone https://github.com/posit-dev/py-shiny.git
@@ -249,7 +249,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-tubular
         run: |
             git clone https://github.com/lvgig/tubular --depth=1
@@ -260,7 +260,7 @@ jobs:
       - name: install-tubular-dev
         run: |
             cd tubular
-            uv pip install -e .[dev] --system
+            uv pip install -e ".[dev]" --system
       - name: install-narwhals-dev
         run: |
             uv pip uninstall narwhals --system
@@ -289,7 +289,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-plotly
         run: |
             git clone https://github.com/plotly/plotly.py --depth=1
@@ -329,7 +329,7 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: clone-hierarchicalforecast
         run: |
             git clone https://github.com/Nixtla/hierarchicalforecast --depth=1
diff --git a/.github/workflows/downstream_tests_slow.yml b/.github/workflows/downstream_tests_slow.yml
index 91aba65a..e26598d8 100644
--- a/.github/workflows/downstream_tests_slow.yml
+++ b/.github/workflows/downstream_tests_slow.yml
@@ -25,7 +25,7 @@ jobs:
           with:
             enable-cache: "true"
             cache-suffix: ${{ matrix.python-version }}
-            cache-dependency-glob: "**requirements*.txt"
+            cache-dependency-glob: "pyproject.toml"
         - name: clone-vegafusion
           run: |
               git clone --single-branch -b v2 https://github.com/vega/vegafusion.git
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 406778fa..8fd4d184 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -23,9 +23,9 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: show-deps
         run: uv pip freeze
       - name: Run pytest
@@ -48,11 +48,13 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
+      - name: install-dask
+        run: uv pip install --upgrade dask[dataframe] --system
       - name: install-modin
-        run: uv pip install --upgrade modin[dask] --system
+        run: uv pip install --upgrade modin --system
       - name: show-deps
         run: uv pip freeze
       - name: Run pytest
@@ -75,9 +77,9 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: install-modin
         run: uv pip install --upgrade modin[dask] --system
       - name: show-deps
diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml
index ffaa5da0..599dab3c 100644
--- a/.github/workflows/random_ci_pytest.yml
+++ b/.github/workflows/random_ci_pytest.yml
@@ -21,13 +21,13 @@ jobs:
         with:
           enable-cache: "true"
           cache-suffix: ${{ matrix.python-version }}
-          cache-dependency-glob: "**requirements*.txt"
+          cache-dependency-glob: "pyproject.toml"
       - name: install package
         run: uv pip install -e . --system
       - name: generate-random-versions
         run: python utils/generate_random_versions.py
       - name: install-reqs
-        run: uv pip install --upgrade tox virtualenv setuptools --system && uv pip install -r requirements-dev.txt --system
+        run: uv pip install -e ".[dev]" --system
       - name: uninstall scipy/sklearn
         run: uv pip uninstall scipy scikit-learn --system
       - name: install-random-verions
diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py
index 21292662..51782794 100644
--- a/tests/expr_and_series/dt/timestamp_test.py
+++ b/tests/expr_and_series/dt/timestamp_test.py
@@ -199,6 +199,7 @@ def test_timestamp_invalid_unit_series(constructor_eager: ConstructorEager) -> N
 )
 @pytest.mark.skipif(PANDAS_VERSION < (2, 2), reason="bug in old pandas")
 @pytest.mark.skipif(POLARS_VERSION < (0, 20, 7), reason="bug in old Polars")
+@pytest.mark.slow
 def test_timestamp_hypothesis(
     inputs: datetime,
     time_unit: Literal["ms", "us", "ns"],
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index e4ab273c..54593421 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -1,10 +1,10 @@
 from __future__ import annotations
 
-from contextlib import nullcontext as does_not_raise
-
+import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import PANDAS_VERSION
 from tests.utils import Constructor
 from tests.utils import assert_equal_data
 
@@ -15,7 +15,10 @@ data = {
 }
 
 
-def test_over_single(constructor: Constructor) -> None:
+def test_over_single(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
     df = nw.from_native(constructor(data))
     expected = {
         "a": ["a", "a", "b", "b", "b"],
@@ -24,21 +27,14 @@ def test_over_single(constructor: Constructor) -> None:
         "c_max": [5, 5, 3, 3, 3],
     }
 
-    context = (
-        pytest.raises(
-            NotImplementedError,
-            match="`Expr.over` is not supported for Dask backend with multiple partitions.",
-        )
-        if "dask_lazy_p2" in str(constructor)
-        else does_not_raise()
-    )
+    result = df.with_columns(c_max=nw.col("c").max().over("a"))
+    assert_equal_data(result, expected)
 
-    with context:
-        result = df.with_columns(c_max=nw.col("c").max().over("a"))
-        assert_equal_data(result, expected)
 
+def test_over_multiple(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
 
-def test_over_multiple(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     expected = {
         "a": ["a", "a", "b", "b", "b"],
@@ -47,18 +43,8 @@ def test_over_multiple(constructor: Constructor) -> None:
         "c_min": [5, 4, 1, 2, 1],
     }
 
-    context = (
-        pytest.raises(
-            NotImplementedError,
-            match="`Expr.over` is not supported for Dask backend with multiple partitions.",
-        )
-        if "dask_lazy_p2" in str(constructor)
-        else does_not_raise()
-    )
-
-    with context:
-        result = df.with_columns(c_min=nw.col("c").min().over("a", "b"))
-        assert_equal_data(result, expected)
+    result = df.with_columns(c_min=nw.col("c").min().over("a", "b"))
+    assert_equal_data(result, expected)
 
 
 def test_over_invalid(request: pytest.FixtureRequest, constructor: Constructor) -> None:
@@ -68,3 +54,121 @@ def test_over_invalid(request: pytest.FixtureRequest, constructor: Constructor)
     df = nw.from_native(constructor(data))
     with pytest.raises(ValueError, match="Anonymous expressions"):
         df.with_columns(c_min=nw.all().min().over("a", "b"))
+
+
+def test_over_cumsum(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    if "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_cumsum": [1, 3, 3, 8, 11],
+    }
+
+    result = df.with_columns(b_cumsum=nw.col("b").cum_sum().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_cumcount(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_cumcount": [1, 2, 1, 2, 3],
+    }
+
+    result = df.with_columns(b_cumcount=nw.col("b").cum_count().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_cumcount_missing_values(
+    request: pytest.FixtureRequest, constructor: Constructor
+) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    data_with_missing_value = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, None],
+        "c": [5, 4, 3, 2, 1],
+    }
+
+    df = nw.from_native(constructor(data_with_missing_value))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, None],
+        "c": [5, 4, 3, 2, 1],
+        "b_cumcount": [1, 2, 1, 2, 2],
+    }
+
+    result = df.with_columns(b_cumcount=nw.col("b").cum_count().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_cummax(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    if "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1):
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_cummax": [1, 2, 3, 5, 5],
+    }
+    result = df.with_columns(b_cummax=nw.col("b").cum_max().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_cummin(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    if "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_cummin": [1, 1, 3, 3, 3],
+    }
+
+    result = df.with_columns(b_cummin=nw.col("b").cum_min().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_cumprod(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "pyarrow_table" in str(constructor) or "dask_lazy_p2" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    if "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_cumprod": [1, 2, 3, 15, 45],
+    }
+
+    result = df.with_columns(b_cumprod=nw.col("b").cum_prod().over("a"))
+    assert_equal_data(result, expected)
+
+
+def test_over_anonymous() -> None:
+    df = pd.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]})
+    with pytest.raises(ValueError, match="Anonymous expressions"):
+        nw.from_native(df).select(nw.all().cum_max().over("a"))
diff --git a/tests/expr_and_series/rolling_mean_test.py b/tests/expr_and_series/rolling_mean_test.py
index 3ffd8749..33c817bf 100644
--- a/tests/expr_and_series/rolling_mean_test.py
+++ b/tests/expr_and_series/rolling_mean_test.py
@@ -82,6 +82,7 @@ def test_rolling_mean_series(constructor_eager: ConstructorEager) -> None:
     values=st.lists(st.floats(-10, 10), min_size=3, max_size=10),
 )
 @pytest.mark.skipif(PANDAS_VERSION < (1,), reason="too old for pyarrow")
+@pytest.mark.slow
 @pytest.mark.filterwarnings("ignore:.*:narwhals.exceptions.NarwhalsUnstableWarning")
 def test_rolling_mean_hypothesis(center: bool, values: list[float]) -> None:  # noqa: FBT001
     s = pd.Series(values)
diff --git a/tests/expr_and_series/rolling_sum_test.py b/tests/expr_and_series/rolling_sum_test.py
index cbf999a0..fae22552 100644
--- a/tests/expr_and_series/rolling_sum_test.py
+++ b/tests/expr_and_series/rolling_sum_test.py
@@ -200,6 +200,7 @@ def test_rolling_sum_series_invalid_params(
 )
 @pytest.mark.skipif(PANDAS_VERSION < (1,), reason="too old for pyarrow")
 @pytest.mark.filterwarnings("ignore:.*:narwhals.exceptions.NarwhalsUnstableWarning")
+@pytest.mark.slow
 def test_rolling_sum_hypothesis(center: bool, values: list[float]) -> None:  # noqa: FBT001
     s = pd.Series(values)
     n_missing = random.randint(0, len(s) - 1)  # noqa: S311
diff --git a/tests/frame/invalid_test.py b/tests/frame/invalid_test.py
index 257dade2..af600321 100644
--- a/tests/frame/invalid_test.py
+++ b/tests/frame/invalid_test.py
@@ -43,6 +43,7 @@ def test_validate_laziness() -> None:
         nw.concat([nw.from_native(df, eager_only=True), nw.from_native(df).lazy()])  # type: ignore[list-item]
 
 
+@pytest.mark.slow
 @pytest.mark.skipif(NUMPY_VERSION < (1, 26, 4), reason="too old")
 def test_memmap() -> None:
     pytest.importorskip("sklearn")
diff --git a/tests/utils_test.py b/tests/utils_test.py
index c36cb168..26bd2ecf 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -247,6 +247,7 @@ def test_get_trivial_version_with_uninstalled_module() -> None:
 
 
 @given(n_bytes=st.integers(1, 100))  # type: ignore[misc]
+@pytest.mark.slow
 def test_generate_temporary_column_name(n_bytes: int) -> None:
     columns = ["abc", "XYZ"]