Repository: narwhals-dev/narwhals

Issue:
bug: can't use `Series.__getitem__` with numpy scalars
The consequence of this is:
```python
In [18]: import pandas as pd

In [19]: import narwhals as nw

In [20]: s = pd.Series([0,1,2])

In [21]: snw = nw.from_native(s, series_only=True)

In [22]: snw[snw.min()]
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[22], line 1
----> 1 snw[snw.min()]

File ~/scratch/.venv/lib/python3.12/site-packages/narwhals/series.py:72, in Series.__getitem__(self, idx)
     70 if isinstance(idx, int):
     71     return self._compliant_series[idx]
---> 72 return self._from_compliant_series(self._compliant_series[idx])

File ~/scratch/.venv/lib/python3.12/site-packages/narwhals/_pandas_like/series.py:128, in PandasLikeSeries.__getitem__(self, idx)
    126 if isinstance(idx, int):
    127     return self._native_series.iloc[idx]
--> 128 return self._from_native_series(self._native_series.iloc[idx])

File ~/scratch/.venv/lib/python3.12/site-packages/narwhals/_pandas_like/series.py:139, in PandasLikeSeries._from_native_series(self, series)
    138 def _from_native_series(self, series: Any) -> Self:
--> 139     return self.__class__(
    140         series,
    141         implementation=self._implementation,
    142         backend_version=self._backend_version,
    143         dtypes=self._dtypes,
    144     )

File ~/scratch/.venv/lib/python3.12/site-packages/narwhals/_pandas_like/series.py:87, in PandasLikeSeries.__init__(self, native_series, implementation, backend_version, dtypes)
     79 def __init__(
     80     self,
     81     native_series: Any,
   (...)
     85     dtypes: DTypes,
     86 ) -> None:
---> 87     self._name = native_series.name
     88     self._native_series = native_series
     89     self._implementation = implementation

AttributeError: 'numpy.int64' object has no attribute 'name'
```

The place this needs changing is in the definition of `Series.__getitem__` for `_pandas_like`

Discussion:
MarcoGorelli: thanks @thevro , looks like there's some test failures
thevro: > thanks @thevro , looks like there's some test failures

The test that fails is the one I added to confirm the exception demonstrated in the PR description. Instead of raising an AttributeError in `narwhals/_pandas_like/series.py`, as described in #1493, we now raise an AssertionError in `narwhals/series.py`. Should I change the test so that it expects the new exception (which is still a bug), or shall we try to pass the test in its current form within this PR?

Here is the reason we still get an error:
Yesterday I initially applied the same approach of `isinstance`-comparing to `numbers.Number` instead of to `int` in _both_ files, and that caused code equivalent to the test I added today to pass. However, it occurred to me that pd.Series can be indexed by other scalars such as strings (scalar != non-Iterable, sadly), so a `np.isscalar` approach is more appropriate. But `narwhals/series.py` must work without numpy installed. I see two possible fixes:

1. if `__getitem__` in `narwhals/_pandas_like/series.py` receives a scalar, the code in `narwhals/series.py` shouldn't even be called.
2. `narwhals/series.py` should also check for scalars in general instead of just `int`s. We would need to find a way to do this without using numpy. 
MarcoGorelli: I think you could just make a function in `narwhals/dependencies.py` called `is_numpy_scalar` in which you just do
```
return (np := get_numpy()) is not None and np.isscalar(obj)
```

Like this, it checks if `obj` is a NumPy scalar without importing NumPy. Like this, you can safely use it in `narwhals/series.py`

Patch:
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 303583a9..bd799039 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -19,6 +19,7 @@ from narwhals._pandas_like.utils import rename
 from narwhals._pandas_like.utils import select_columns_by_name
 from narwhals._pandas_like.utils import set_axis
 from narwhals._pandas_like.utils import to_datetime
+from narwhals.dependencies import is_numpy_scalar
 from narwhals.typing import CompliantSeries
 from narwhals.utils import Implementation
 from narwhals.utils import import_dtypes_module
@@ -114,7 +115,7 @@ class PandasLikeSeries(CompliantSeries):
     def __getitem__(self, idx: slice | Sequence[int]) -> Self: ...
 
     def __getitem__(self, idx: int | slice | Sequence[int]) -> Any | Self:
-        if isinstance(idx, int):
+        if isinstance(idx, int) or is_numpy_scalar(idx):
             return self._native_series.iloc[idx]
         return self._from_native_series(self._native_series.iloc[idx])
 
diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py
index 61937f08..0c5d1172 100644
--- a/narwhals/dependencies.py
+++ b/narwhals/dependencies.py
@@ -223,6 +223,11 @@ def is_numpy_array(arr: Any) -> TypeGuard[np.ndarray]:
     return (np := get_numpy()) is not None and isinstance(arr, np.ndarray)
 
 
+def is_numpy_scalar(scalar: Any) -> TypeGuard[np.generic]:
+    """Check whether `scalar` is a NumPy Scalar without importing NumPy."""
+    return (np := get_numpy()) is not None and np.isscalar(scalar)
+
+
 def is_pandas_like_dataframe(df: Any) -> bool:
     """Check whether `df` is a pandas-like DataFrame without doing any imports.
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 2797ceaf..2ef2efc7 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -11,6 +11,7 @@ from typing import Sequence
 from typing import TypeVar
 from typing import overload
 
+from narwhals.dependencies import is_numpy_scalar
 from narwhals.dtypes import _validate_dtype
 from narwhals.typing import IntoSeriesT
 from narwhals.utils import _validate_rolling_arguments
@@ -160,7 +161,9 @@ class Series(Generic[IntoSeriesT]):
               ]
             ]
         """
-        if isinstance(idx, int):
+        if isinstance(idx, int) or (
+            is_numpy_scalar(idx) and idx.dtype.kind in ("i", "u")
+        ):
             return self._compliant_series[idx]
         return self._from_compliant_series(self._compliant_series[idx])
 


Test patch:
diff --git a/tests/series_only/slice_test.py b/tests/series_only/slice_test.py
index 1eb5e0ba..c4f8dfbe 100644
--- a/tests/series_only/slice_test.py
+++ b/tests/series_only/slice_test.py
@@ -39,3 +39,9 @@ def test_slice(constructor_eager: ConstructorEager) -> None:
 def test_getitem_arrow_scalar() -> None:
     result = nw.from_native(pa.chunked_array([[1]]), series_only=True)[0]
     assert isinstance(result, int)
+
+
+def test_index(constructor_eager: ConstructorEager) -> None:
+    df = constructor_eager({"a": [0, 1, 2]})
+    snw = nw.from_native(df, eager_only=True)["a"]
+    assert snw[snw[0]] == 0