Repository: narwhals-dev/narwhals

Issue:
[Bug]: Tuple row value in subset causes odd error
### Describe the bug

Odd edge case

### Steps or code to reproduce the bug

```python
import narwhals.stable.v1 as nw
import palmerpenguins

pd_df = palmerpenguins.load_penguins_raw().iloc[0:2, 0:2]

nw_df = nw.from_native(pd_df, eager_only=True)

# Works
nw_df[(0,1), nw_df.columns]
nw_df[[0,1], :]

# Fails
try:
    nw_df[(0,1), :]
except AttributeError as e:
    print("nw_df[(0,1), :] error:", str(e))
#> nw_df[(0,1), :] error: 'numpy.int64' object has no attribute 'name'
```

### Expected results

Subsetting works the same as if it was a list or of columns were supplied

### Actual results

Error

### Please run narwhals.show_version() and enter the output below.

```shell
System:
    python: 3.12.6 (main, Sep  6 2024, 19:03:47) [Clang 15.0.0 (clang-1500.3.9.4)]
executable: /Users/barret/Documents/git/rstudio/py-shiny/py-shiny.nosync/venv/bin/python
   machine: macOS-14.6.1-arm64-arm-64bit

Python dependencies:
     narwhals: 1.9.0
       pandas: 2.2.3
       polars: 1.8.1
         cudf: 
        modin: 
      pyarrow: 
        numpy: 2.1.1
```


### Relevant log output

```shell
....
  File "/shiny/render/_data_frame_utils/_tbl_data.py", line 298, in subset_frame
    return data[rows, :]
           ~~~~^^^^^^^^^
  File "/venv/lib/python3.12/site-packages/narwhals/stable/v1/__init__.py", line 128, in __getitem__
    return super().__getitem__(item)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/venv/lib/python3.12/site-packages/narwhals/dataframe.py", line 847, in __getitem__
    return self._from_compliant_dataframe(self._compliant_frame[item[0]])
                                          ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/venv/lib/python3.12/site-packages/narwhals/_pandas_like/dataframe.py", line 215, in __getitem__
    return PandasLikeSeries(
           ^^^^^^^^^^^^^^^^^
  File "/venv/lib/python3.12/site-packages/narwhals/_pandas_like/series.py", line 84, in __init__
    self._name = native_series.name
                 ^^^^^^^^^^^^^^^^^^
AttributeError: 'numpy.int64' object has no attribute 'name'
```


Discussion:

Patch:
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index efc34317..a46c6394 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -176,6 +176,10 @@ class ArrowDataFrame:
         elif isinstance(item, tuple) and len(item) == 2:
             if isinstance(item[1], slice):
                 columns = self.columns
+                if item[1] == slice(None):
+                    if isinstance(item[0], Sequence) and len(item[0]) == 0:
+                        return self._from_native_frame(self._native_frame.slice(0, 0))
+                    return self._from_native_frame(self._native_frame.take(item[0]))
                 if isinstance(item[1].start, str) or isinstance(item[1].stop, str):
                     start, stop, step = convert_str_slice_to_int_slice(item[1], columns)
                     return self._from_native_frame(
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index aae86cef..e808545a 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -186,6 +186,8 @@ class PandasLikeDataFrame:
 
         elif isinstance(item, tuple) and len(item) == 2 and isinstance(item[1], slice):
             columns = self._native_frame.columns
+            if item[1] == slice(None):
+                return self._from_native_frame(self._native_frame.iloc[item[0], :])
             if isinstance(item[1].start, str) or isinstance(item[1].stop, str):
                 start, stop, step = convert_str_slice_to_int_slice(item[1], columns)
                 return self._from_native_frame(
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index a4e30ec6..0ef2f879 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 from typing import Any
+from typing import Sequence
 
 from narwhals._polars.namespace import PolarsNamespace
 from narwhals._polars.utils import convert_str_slice_to_int_slice
@@ -117,6 +118,12 @@ class PolarsDataFrame:
 
             columns = self.columns
             if isinstance(item, tuple) and len(item) == 2 and isinstance(item[1], slice):
+                if item[1] == slice(None):
+                    if isinstance(item[0], Sequence) and not len(item[0]):
+                        return self._from_native_frame(self._native_frame[0:0])
+                    return self._from_native_frame(
+                        self._native_frame.__getitem__(item[0])
+                    )
                 if isinstance(item[1].start, str) or isinstance(item[1].stop, str):
                     start, stop, step = convert_str_slice_to_int_slice(item[1], columns)
                     return self._from_native_frame(
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 0ee778ab..e4627484 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -843,8 +843,6 @@ class DataFrame(BaseFrame[DataFrameT]):
         ):
             if item[1] == slice(None) and item[0] == slice(None):
                 return self
-            if item[1] == slice(None):
-                return self._from_compliant_dataframe(self._compliant_frame[item[0]])
             return self._from_compliant_dataframe(self._compliant_frame[item])
         if isinstance(item, str) or (isinstance(item, tuple) and len(item) == 2):
             return self._series(


Test patch:
diff --git a/tests/frame/getitem_test.py b/tests/frame/getitem_test.py
index 894555b3..ce96c1b2 100644
--- a/tests/frame/getitem_test.py
+++ b/tests/frame/getitem_test.py
@@ -177,6 +177,15 @@ def test_slice_slice_columns(constructor_eager: Any) -> None:  # noqa: PLR0915
     result = df["a":"b"]  # type: ignore[misc]
     expected = {"a": [1, 2, 3], "b": [4, 5, 6]}
     compare_dicts(result, expected)
+    result = df[(0, 1), :]
+    expected = {"a": [1, 2], "b": [4, 5], "c": [7, 8], "d": [1, 4]}
+    compare_dicts(result, expected)
+    result = df[[0, 1], :]
+    expected = {"a": [1, 2], "b": [4, 5], "c": [7, 8], "d": [1, 4]}
+    compare_dicts(result, expected)
+    result = df[[0, 1], df.columns]
+    expected = {"a": [1, 2], "b": [4, 5], "c": [7, 8], "d": [1, 4]}
+    compare_dicts(result, expected)
 
 
 def test_slice_invalid(constructor_eager: Any) -> None: