Repository: narwhals-dev/narwhals (python)

Issue:
deprecate LazyFrame.gather_every (but keep around in stable.v1)
towards https://github.com/narwhals-dev/narwhals/discussions/1657

Pull request:
chore: deprecate `LazyFrame.gather_every` in main namespace (but maintain it in stable.v1)
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [x] 🐳 Other

## Related issues

- Closes #2047

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below


Patch:
diff --git a/docs/backcompat.md b/docs/backcompat.md
index 3c20c7e4..e1ffa329 100644
--- a/docs/backcompat.md
+++ b/docs/backcompat.md
@@ -113,6 +113,8 @@ before making any change.
 
 The following are differences between the main Narwhals namespace and `narwhals.stable.v1`:
 
+- Since Narwhals 1.29.0, `LazyFrame.gather_every` has been deprecated from the main namespace.
+
 - Since Narwhals 1.24.1, an empty or all-null object-dtype pandas Series is inferred to
   be of dtype `String`. Previously, it would have been inferred as `Object`.
 
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index b9a279be..8825f2b5 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -34,6 +34,7 @@ from narwhals.utils import find_stacklevel
 from narwhals.utils import flatten
 from narwhals.utils import generate_repr
 from narwhals.utils import is_sequence_but_not_str
+from narwhals.utils import issue_deprecation_warning
 from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
@@ -3140,6 +3141,11 @@ class LazyFrame(BaseFrame[FrameT]):
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
         r"""Take every nth row in the DataFrame and return as a new DataFrame.
 
+        !!! warning
+            `LazyFrame.gather_every` is deprecated and will be removed in a future version.
+            Note: this will remain available in `narwhals.stable.v1`.
+            See [stable api](../backcompat.md/) for more information.
+
         Arguments:
             n: Gather every *n*-th row.
             offset: Starting index.
@@ -3147,6 +3153,13 @@ class LazyFrame(BaseFrame[FrameT]):
         Returns:
             The LazyFrame containing only the selected rows.
         """
+        msg = (
+            "`LazyFrame.gather_every` is deprecated and will be removed in a future version.\n\n"
+            "Note: this will remain available in `narwhals.stable.v1`.\n"
+            "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
+        )
+        issue_deprecation_warning(msg, _version="1.29.0")
+
         return super().gather_every(n=n, offset=offset)
 
     def unpivot(
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 04b244be..62ceab1f 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1096,7 +1096,7 @@ class Expr:
             "Note: this will remain available in `narwhals.stable.v1`.\n"
             "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
         )
-        issue_deprecation_warning(msg, _version="1.22.0")
+        issue_deprecation_warning(msg, _version="1.23.0")
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).sort(
                 descending=descending, nulls_last=nulls_last
@@ -1485,7 +1485,7 @@ class Expr:
             "Note: this will remain available in `narwhals.stable.v1`.\n"
             "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
         )
-        issue_deprecation_warning(msg, _version="1.22.0")
+        issue_deprecation_warning(msg, _version="1.23.0")
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).sample(
                 n, fraction=fraction, with_replacement=with_replacement, seed=seed
@@ -1753,7 +1753,7 @@ class Expr:
             "Note: this will remain available in `narwhals.stable.v1`.\n"
             "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
         )
-        issue_deprecation_warning(msg, _version="1.22.0")
+        issue_deprecation_warning(msg, _version="1.23.0")
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).head(n),
             self._metadata.with_kind_and_extra_open_window(ExprKind.FILTRATION),
@@ -1781,7 +1781,7 @@ class Expr:
             "Note: this will remain available in `narwhals.stable.v1`.\n"
             "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
         )
-        issue_deprecation_warning(msg, _version="1.22.0")
+        issue_deprecation_warning(msg, _version="1.23.0")
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).tail(n),
             self._metadata.with_kind_and_extra_open_window(ExprKind.FILTRATION),
@@ -1876,7 +1876,7 @@ class Expr:
             "Note: this will remain available in `narwhals.stable.v1`.\n"
             "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
         )
-        issue_deprecation_warning(msg, _version="1.22.0")
+        issue_deprecation_warning(msg, _version="1.23.0")
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).gather_every(n=n, offset=offset),
             self._metadata.with_kind_and_extra_open_window(ExprKind.FILTRATION),
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index cececde6..8a0e2b13 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -348,6 +348,20 @@ class LazyFrame(NwLazyFrame[IntoFrameT]):
         """
         return super().tail(n)
 
+    def gather_every(self: Self, n: int, offset: int = 0) -> Self:
+        r"""Take every nth row in the DataFrame and return as a new DataFrame.
+
+        Arguments:
+            n: Gather every *n*-th row.
+            offset: Starting index.
+
+        Returns:
+            The LazyFrame containing only the selected rows.
+        """
+        return self._from_compliant_dataframe(
+            self._compliant_frame.gather_every(n=n, offset=offset)
+        )
+
 
 class Series(NwSeries[IntoSeriesT]):
     """Narwhals Series, backed by a native series.


Test patch:
diff --git a/tests/expr_and_series/arg_true_test.py b/tests/expr_and_series/arg_true_test.py
index ec7d1d88..35c6b9bd 100644
--- a/tests/expr_and_series/arg_true_test.py
+++ b/tests/expr_and_series/arg_true_test.py
@@ -14,7 +14,9 @@ def test_arg_true(constructor_eager: ConstructorEager) -> None:
     expected = {"a": [1, 2]}
     assert_equal_data(result, expected)
 
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(nw.col("a").is_null().arg_true())
 
 
diff --git a/tests/expr_and_series/gather_every_test.py b/tests/expr_and_series/gather_every_test.py
index bcda13d8..7725b287 100644
--- a/tests/expr_and_series/gather_every_test.py
+++ b/tests/expr_and_series/gather_every_test.py
@@ -22,7 +22,9 @@ def test_gather_every_expr(
 
     assert_equal_data(result, expected)
 
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(nw_main.col("a").gather_every(n=n, offset=offset))
 
 
diff --git a/tests/expr_and_series/head_test.py b/tests/expr_and_series/head_test.py
index 952b3de1..8b5af5c7 100644
--- a/tests/expr_and_series/head_test.py
+++ b/tests/expr_and_series/head_test.py
@@ -19,7 +19,9 @@ def test_head(
     expected = {"a": [1, 2]}
     assert_equal_data(result, expected)
 
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(nw_main.col("a").head(5))
 
 
diff --git a/tests/expr_and_series/sample_test.py b/tests/expr_and_series/sample_test.py
index af1f5665..e60142f9 100644
--- a/tests/expr_and_series/sample_test.py
+++ b/tests/expr_and_series/sample_test.py
@@ -21,7 +21,9 @@ def test_expr_sample(constructor_eager: ConstructorEager) -> None:
     expected_series = (2,)
     assert result_series == expected_series
 
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(nw_main.col("a").sample(n=2))
 
 
diff --git a/tests/expr_and_series/sort_test.py b/tests/expr_and_series/sort_test.py
index e35514cd..e3e66592 100644
--- a/tests/expr_and_series/sort_test.py
+++ b/tests/expr_and_series/sort_test.py
@@ -30,7 +30,9 @@ def test_sort_expr(
         nw.col("b").sort(descending=descending, nulls_last=nulls_last),
     )
     assert_equal_data(result, expected)
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(
             "a",
             nw_main.col("b").sort(descending=descending, nulls_last=nulls_last),
diff --git a/tests/expr_and_series/tail_test.py b/tests/expr_and_series/tail_test.py
index 2baabfd9..ac857dee 100644
--- a/tests/expr_and_series/tail_test.py
+++ b/tests/expr_and_series/tail_test.py
@@ -19,7 +19,9 @@ def test_tail(
     expected = {"a": [2, 3]}
     assert_equal_data(result, expected)
 
-    with pytest.deprecated_call():
+    with pytest.deprecated_call(
+        match="is deprecated and will be removed in a future version"
+    ):
         df.select(nw_main.col("a").tail(5))
 
 
diff --git a/tests/frame/gather_every_test.py b/tests/frame/gather_every_test.py
index c151f450..b4568671 100644
--- a/tests/frame/gather_every_test.py
+++ b/tests/frame/gather_every_test.py
@@ -1,7 +1,10 @@
 from __future__ import annotations
 
+from contextlib import nullcontext
+
 import pytest
 
+import narwhals as nw_main
 import narwhals.stable.v1 as nw
 from tests.utils import Constructor
 from tests.utils import assert_equal_data
@@ -14,9 +17,23 @@ data = {"a": list(range(10))}
 def test_gather_every(
     constructor: Constructor, n: int, offset: int, request: pytest.FixtureRequest
 ) -> None:
-    if ("pyspark" in str(constructor)) or "duckdb" in str(constructor):
+    if "pyspark" in str(constructor) or "duckdb" in str(constructor):
         request.applymarker(pytest.mark.xfail)
-    df = nw.from_native(constructor(data))
-    result = df.gather_every(n=n, offset=offset)
+    df_v1 = nw.from_native(constructor(data))
+    result = df_v1.gather_every(n=n, offset=offset)
     expected = {"a": data["a"][offset::n]}
     assert_equal_data(result, expected)
+
+    # Test deprecation for LazyFrame in main namespace
+    df_main = nw_main.from_native(constructor(data))
+
+    context = (
+        pytest.deprecated_call(
+            match="is deprecated and will be removed in a future version"
+        )
+        if isinstance(df_main, nw_main.LazyFrame)
+        else nullcontext()
+    )
+
+    with context:
+        df_main.gather_every(n=n, offset=offset)
diff --git a/tests/stable_api_test.py b/tests/stable_api_test.py
index 071065e6..2f0277a1 100644
--- a/tests/stable_api_test.py
+++ b/tests/stable_api_test.py
@@ -122,7 +122,7 @@ def test_lazyframe_docstrings() -> None:
         if item in {"schema", "columns"}:
             # to avoid performance warning
             continue
-        if item in {"tail"}:
+        if item in {"tail", "gather_every"}:
             # deprecated
             continue
         assert remove_docstring_examples(