Repository: narwhals-dev/narwhals
Pull request #1056 (2024-09-23 23:27:29)

fix: add cuDF specific implementation for join how="anti"
<!--
# 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
- [x] 🐛 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.

Previous PR: https://github.com/narwhals-dev/narwhals/pull/1041

Looks like cuDF doesn't need this bit where we drop duplicates 
https://github.com/narwhals-dev/narwhals/pull/1041/files#diff-1bb9576cb79de69b2e45bcbc828669f54a6049dbdb76407986b3439ba309369dR481-R487

Tested and it's working as expected
<img width="653" alt="image" src="https://github.com/user-attachments/assets/82eae917-9989-4ee9-831c-97c9ff6931b8">



Patch:
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 8f4c9881..d8928bce 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -1,6 +1,7 @@
 exclude-labels:
-  - skip changelog
+  - ignore
   - release
+  - dependencies
 name-template: 'Narwhals v$RESOLVED_VERSION'
 
 change-template: '- $TITLE (#$NUMBER)' 
@@ -34,6 +35,9 @@ autolabeler:
   - label: release
     title:
       - '/^([Rr]elease)/'
+  - label: ignore
+    title:
+      - '/^\[pre-commit.ci\]/'
       
 version-resolver:
   major:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e97fce29..08d274f8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: 'v0.6.5'
+  rev: 'v0.6.7'
   hooks:
     # Run the formatter.
     - id: ruff-format
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 9868e7b9..5adaa57a 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -6,6 +6,7 @@
       members:
         - __arrow_c_stream__
         - __getitem__
+        - __iter__
         - abs
         - alias
         - all
diff --git a/docs/installation.md b/docs/installation.md
index 2f801254..28a07d5b 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -13,7 +13,7 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.8.2'
+'1.8.3'
 ```
 then installation worked correctly!
 
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index efc5cd8c..1313f113 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -53,7 +53,7 @@ from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_get_index
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.8.2"
+__version__ = "1.8.3"
 
 __all__ = [
     "dependencies",
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 09fade68..8c864379 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
+from typing import Iterator
 from typing import Literal
 from typing import Sequence
 from typing import overload
@@ -702,6 +703,9 @@ class ArrowSeries:
             plx.col(col_token) == plx.col(col_token).max()
         )[self.name]
 
+    def __iter__(self: Self) -> Iterator[Any]:
+        yield from self._native_series.__iter__()
+
     @property
     def shape(self) -> tuple[int]:
         return (len(self._native_series),)
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index 10d686ab..3ccf906f 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -12,7 +12,6 @@ from typing import cast
 from typing import overload
 
 from narwhals.dependencies import is_numpy_array
-from narwhals.utils import flatten
 
 if TYPE_CHECKING:
     from narwhals._arrow.dataframe import ArrowDataFrame
@@ -95,7 +94,7 @@ def evaluate_into_exprs(
     """Evaluate each expr into Series."""
     series: ListOfCompliantSeries = [  # type: ignore[assignment]
         item
-        for sublist in (evaluate_into_expr(df, into_expr) for into_expr in flatten(exprs))
+        for sublist in (evaluate_into_expr(df, into_expr) for into_expr in exprs)
         for item in sublist
     ]
     for name, expr in named_exprs.items():
@@ -157,9 +156,7 @@ def parse_into_exprs(
 ) -> ListOfCompliantExpr:
     """Parse each input as an expression (if it's not already one). See `parse_into_expr` for
     more details."""
-    return [
-        parse_into_expr(into_expr, namespace=namespace) for into_expr in flatten(exprs)
-    ] + [
+    return [parse_into_expr(into_expr, namespace=namespace) for into_expr in exprs] + [
         parse_into_expr(expr, namespace=namespace).alias(name)
         for name, expr in named_exprs.items()
     ]
@@ -181,7 +178,6 @@ def parse_into_expr(
     - if it's a string, then convert it to an expression
     - else, raise
     """
-
     if hasattr(into_expr, "__narwhals_expr__"):
         return into_expr  # type: ignore[return-value]
     if hasattr(into_expr, "__narwhals_series__"):
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 446c68a1..09742972 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -474,28 +474,38 @@ class PandasLikeDataFrame:
                 )
 
         if how == "anti":
-            indicator_token = generate_unique_token(
-                n_bytes=8, columns=[*self.columns, *other.columns]
-            )
+            if self._implementation is Implementation.CUDF:  # pragma: no cover
+                return self._from_native_frame(
+                    self._native_frame.merge(
+                        other._native_frame,
+                        how="leftanti",
+                        left_on=left_on,
+                        right_on=right_on,
+                    )
+                )
+            else:
+                indicator_token = generate_unique_token(
+                    n_bytes=8, columns=[*self.columns, *other.columns]
+                )
 
-            other_native = (
-                other._native_frame.loc[:, right_on]
-                .rename(  # rename to avoid creating extra columns in join
-                    columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
+                other_native = (
+                    other._native_frame.loc[:, right_on]
+                    .rename(  # rename to avoid creating extra columns in join
+                        columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
+                    )
+                    .drop_duplicates()
                 )
-                .drop_duplicates()
-            )
-            return self._from_native_frame(
-                self._native_frame.merge(
-                    other_native,
-                    how="outer",
-                    indicator=indicator_token,
-                    left_on=left_on,
-                    right_on=left_on,
+                return self._from_native_frame(
+                    self._native_frame.merge(
+                        other_native,
+                        how="outer",
+                        indicator=indicator_token,
+                        left_on=left_on,
+                        right_on=left_on,
+                    )
+                    .loc[lambda t: t[indicator_token] == "left_only"]
+                    .drop(columns=indicator_token)
                 )
-                .loc[lambda t: t[indicator_token] == "left_only"]
-                .drop(columns=indicator_token)
-            )
 
         if how == "semi":
             other_native = (
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 1b40d69d..873683dd 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
+from typing import Iterator
 from typing import Literal
 from typing import Sequence
 from typing import overload
@@ -665,6 +666,9 @@ class PandasLikeSeries:
         result.name = native_series.name
         return self._from_native_series(result)
 
+    def __iter__(self: Self) -> Iterator[Any]:
+        yield from self._native_series.__iter__()
+
     @property
     def str(self) -> PandasLikeSeriesStringNamespace:
         return PandasLikeSeriesStringNamespace(self)
diff --git a/narwhals/series.py b/narwhals/series.py
index 619e4b55..ae78eef7 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
+from typing import Iterator
 from typing import Literal
 from typing import Sequence
 from typing import overload
@@ -2407,6 +2408,9 @@ class Series:
         """
         return self._from_compliant_series(self._compliant_series.mode())
 
+    def __iter__(self: Self) -> Iterator[Any]:
+        yield from self._compliant_series.__iter__()
+
     @property
     def str(self) -> SeriesStringNamespace:
         return SeriesStringNamespace(self)
diff --git a/pyproject.toml b/pyproject.toml
index f2109e11..13128e4f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.8.2"
+version = "1.8.3"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]
diff --git a/utils/bump_version.py b/utils/bump_version.py
index 1f686989..4230e689 100644
--- a/utils/bump_version.py
+++ b/utils/bump_version.py
@@ -4,9 +4,33 @@ import re
 import subprocess
 import sys
 
-subprocess.run(["git", "fetch", "upstream"])
+out = subprocess.run(["git", "fetch", "upstream", "--tags"])
+if out.returncode != 0:
+    print(
+        "Something went wrong with the release process, please check the Narwhals Wiki and try again."
+    )
+    print(out)
+    sys.exit(1)
 subprocess.run(["git", "reset", "--hard", "upstream/main"])
 
+# Delete local tags, if present
+try:
+    # Get the list of all tags
+    result = subprocess.run(
+        ["git", "tag", "-l"], capture_output=True, text=True, check=True
+    )
+    tags = result.stdout.splitlines()  # Split the tags into a list by lines
+
+    # Delete each tag using git tag -d
+    for tag in tags:
+        subprocess.run(["git", "tag", "-d", tag], check=True)
+    print("All local tags have been deleted.")
+except subprocess.CalledProcessError as e:
+    print(f"An error occurred: {e}")
+
+subprocess.run(["git", "fetch", "upstream", "--tags"])
+subprocess.run(["git", "fetch", "upstream", "--prune", "--tags"])
+
 how = sys.argv[1]
 
 with open("pyproject.toml", encoding="utf-8") as f:
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index a56b28c5..d11c2bc9 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -28,6 +28,7 @@ SERIES_ONLY_METHODS = {
     "item",
     "scatter",
     "to_native",
+    "__iter__",
 }
 BASE_DTYPES = {"NumericType", "DType", "TemporalType"}
 


Test patch:
diff --git a/tests/series_only/__iter___test.py b/tests/series_only/__iter___test.py
new file mode 100644
index 00000000..d190cd80
--- /dev/null
+++ b/tests/series_only/__iter___test.py
@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+from collections.abc import Iterable
+from typing import Any
+
+import narwhals.stable.v1 as nw
+from tests.utils import compare_dicts
+
+data = [1, 2, 3]
+
+
+def test_to_list(constructor_eager: Any) -> None:
+    s = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"]
+
+    assert isinstance(s, Iterable)
+    compare_dicts({"a": [x for x in s]}, {"a": [1, 2, 3]})  # noqa: C416