Repository: narwhals-dev/narwhals (python)

Issue:
feat: make `dependencies` available under `narwhals.stable.v1` for type checkers
Currently
```
from narwhals.stable.v1.dependencies import is_pandas_like_dataframe
````
raises, but 
```
from narwhals.stable.v1 import dependencies
```
passes

Pull request description:
feat: make sure `from narwhals.stable.v1.dependencies` import works
closes #1426 

<!--
# 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/narwhals/dataframe.py b/narwhals/dataframe.py
index b0d9adbd..8bdb195f 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -962,21 +962,24 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoDataFrame
+            >>> from typing import Any
+            >>>
             >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
 
             Let's define a library-agnostic function to get the second row.
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_row(df_native: IntoDataFrame) -> tuple[Any, ...]:
+            ...     df = nw.from_native(df_native)
             ...     return df.row(1)
 
             We can then pass pandas / Polars / any other supported library:
 
-            >>> func(df_pd)
+            >>> agnostic_row(df_pd)
             (2, 5)
-            >>> func(df_pl)
+            >>> agnostic_row(df_pl)
             (2, 5)
         """
         return self._compliant_frame.row(index)  # type: ignore[no-any-return]
@@ -989,26 +992,30 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2, 3], "ba": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_pipe(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
             ...     return df.pipe(
-            ...         lambda _df: _df.select([x for x in _df.columns if len(x) == 1])
+            ...         lambda _df: _df.select(
+            ...             [x for x in _df.columns if len(x) == 1]
+            ...         ).to_native()
             ...     )
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_pipe(df_pd)
                a
             0  1
             1  2
             2  3
-            >>> func(df_pl)
+            >>> agnostic_pipe(df_pl)
             shape: (3, 1)
             ┌─────┐
             │ a   │
@@ -1037,22 +1044,24 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1.0, 2.0, None], "ba": [1.0, None, 2.0]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop_nulls()
+            >>> def agnostic_drop_nulls(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.drop_nulls().to_native()
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_drop_nulls(df_pd)
                  a   ba
             0  1.0  1.0
-            >>> func(df_pl)
+            >>> agnostic_drop_nulls(df_pl)
             shape: (1, 2)
             ┌─────┬─────┐
             │ a   ┆ ba  │
@@ -1073,24 +1082,26 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_row_index()
+            >>> def agnostic_with_row_index(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.with_row_index().to_native()
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_with_row_index(df_pd)
                index  a  b
             0      0  1  4
             1      1  2  5
             2      2  3  6
-            >>> func(df_pl)
+            >>> agnostic_with_row_index(df_pl)
             shape: (3, 3)
             ┌───────┬─────┬─────┐
             │ index ┆ a   ┆ b   │
@@ -1112,6 +1123,9 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.schema import Schema
+            >>> from narwhals.typing import IntoFrame
+            >>>
             >>> data = {
             ...     "foo": [1, 2, 3],
             ...     "bar": [6.0, 7.0, 8.0],
@@ -1122,17 +1136,17 @@ class DataFrame(BaseFrame[DataFrameT]):
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_schema(df_native: IntoFrame) -> Schema:
+            ...     df = nw.from_native(df_native)
             ...     return df.schema
 
-            You can pass either pandas or Polars to `func`:
+            You can pass either pandas or Polars to `agnostic_schema`:
 
-            >>> df_pd_schema = func(df_pd)
+            >>> df_pd_schema = agnostic_schema(df_pd)
             >>> df_pd_schema
             Schema({'foo': Int64, 'bar': Float64, 'ham': String})
 
-            >>> df_pl_schema = func(df_pl)
+            >>> df_pl_schema = agnostic_schema(df_pl)
             >>> df_pl_schema
             Schema({'foo': Int64, 'bar': Float64, 'ham': String})
         """
@@ -1145,6 +1159,9 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.schema import Schema
+            >>> from narwhals.typing import IntoFrame
+            >>>
             >>> data = {
             ...     "foo": [1, 2, 3],
             ...     "bar": [6.0, 7.0, 8.0],
@@ -1155,17 +1172,17 @@ class DataFrame(BaseFrame[DataFrameT]):
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_collect_schema(df_native: IntoFrame) -> Schema:
+            ...     df = nw.from_native(df_native)
             ...     return df.collect_schema()
 
-            You can pass either pandas or Polars to `func`:
+            You can pass either pandas or Polars to `agnostic_collect_schema`:
 
-            >>> df_pd_schema = func(df_pd)
+            >>> df_pd_schema = agnostic_collect_schema(df_pd)
             >>> df_pd_schema
             Schema({'foo': Int64, 'bar': Float64, 'ham': String})
 
-            >>> df_pl_schema = func(df_pl)
+            >>> df_pl_schema = agnostic_collect_schema(df_pl)
             >>> df_pl_schema
             Schema({'foo': Int64, 'bar': Float64, 'ham': String})
         """
@@ -1180,6 +1197,8 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrame
+            >>>
             >>> df = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(df)
             >>> df_pl = pl.DataFrame(df)
@@ -1187,17 +1206,17 @@ class DataFrame(BaseFrame[DataFrameT]):
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_columns(df_native: IntoFrame) -> list[str]:
+            ...     df = nw.from_native(df_native)
             ...     return df.columns
 
             We can pass any supported library such as pandas, Polars, or PyArrow to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_columns(df_pd)
             ['foo', 'bar', 'ham']
-            >>> func(df_pl)
+            >>> agnostic_columns(df_pl)
             ['foo', 'bar', 'ham']
-            >>> func(df_pa)
+            >>> agnostic_columns(df_pa)
             ['foo', 'bar', 'ham']
         """
         return super().columns
@@ -2984,24 +3003,26 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2, 3], "ba": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.pipe(lambda _df: _df.select("a"))
+            >>> def agnostic_pipe(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.pipe(lambda _df: _df.select("a")).to_native()
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_pipe(df_pd)
                a
             0  1
             1  2
             2  3
-            >>> func(df_pl).collect()
+            >>> agnostic_pipe(df_pl).collect()
             shape: (3, 1)
             ┌─────┐
             │ a   │
@@ -3030,22 +3051,24 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1.0, 2.0, None], "ba": [1.0, None, 2.0]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop_nulls()
+            >>> def agnostic_drop_nulls(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.drop_nulls().to_native()
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_drop_nulls(df_pd)
                  a   ba
             0  1.0  1.0
-            >>> func(df_pl).collect()
+            >>> agnostic_drop_nulls(df_pl).collect()
             shape: (1, 2)
             ┌─────┬─────┐
             │ a   ┆ ba  │
@@ -3064,24 +3087,26 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import polars as pl
             >>> import pandas as pd
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_row_index()
+            >>> def agnostic_with_row_index(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.with_row_index().to_native()
 
             We can then pass either pandas or Polars:
 
-            >>> func(df_pd)
+            >>> agnostic_with_row_index(df_pd)
                index  a  b
             0      0  1  4
             1      1  2  5
             2      2  3  6
-            >>> func(df_pl).collect()
+            >>> agnostic_with_row_index(df_pl).collect()
             shape: (3, 3)
             ┌───────┬─────┬─────┐
             │ index ┆ a   ┆ b   │
@@ -3142,21 +3167,23 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrame
+            >>>
             >>> df = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(df)
             >>> lf_pl = pl.LazyFrame(df)
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
+            >>> def agnostic_columns(df_native: IntoFrame) -> list[str]:
+            ...     df = nw.from_native(df_native)
             ...     return df.columns
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass either pandas or Polars to `agnostic_columns`:
 
-            >>> func(df_pd)
+            >>> agnostic_columns(df_pd)
             ['foo', 'bar', 'ham']
-            >>> func(lf_pl)  # doctest: +SKIP
+            >>> agnostic_columns(lf_pl)  # doctest: +SKIP
             ['foo', 'bar', 'ham']
         """
         return super().columns
diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py
index 3f5e1838..99793161 100644
--- a/narwhals/dependencies.py
+++ b/narwhals/dependencies.py
@@ -295,6 +295,7 @@ __all__ = [
     "is_ibis_table",
     "is_pandas_dataframe",
     "is_pandas_series",
+    "is_pandas_index",
     "is_polars_dataframe",
     "is_polars_lazyframe",
     "is_polars_series",
diff --git a/narwhals/stable/v1/dependencies.py b/narwhals/stable/v1/dependencies.py
new file mode 100644
index 00000000..3c3fae32
--- /dev/null
+++ b/narwhals/stable/v1/dependencies.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from narwhals.dependencies import get_cudf
+from narwhals.dependencies import get_ibis
+from narwhals.dependencies import get_modin
+from narwhals.dependencies import get_numpy
+from narwhals.dependencies import get_pandas
+from narwhals.dependencies import get_polars
+from narwhals.dependencies import get_pyarrow
+from narwhals.dependencies import is_cudf_dataframe
+from narwhals.dependencies import is_cudf_series
+from narwhals.dependencies import is_dask_dataframe
+from narwhals.dependencies import is_ibis_table
+from narwhals.dependencies import is_into_dataframe
+from narwhals.dependencies import is_into_series
+from narwhals.dependencies import is_modin_dataframe
+from narwhals.dependencies import is_modin_series
+from narwhals.dependencies import is_numpy_array
+from narwhals.dependencies import is_pandas_dataframe
+from narwhals.dependencies import is_pandas_index
+from narwhals.dependencies import is_pandas_like_dataframe
+from narwhals.dependencies import is_pandas_like_series
+from narwhals.dependencies import is_pandas_series
+from narwhals.dependencies import is_polars_dataframe
+from narwhals.dependencies import is_polars_lazyframe
+from narwhals.dependencies import is_polars_series
+from narwhals.dependencies import is_pyarrow_chunked_array
+from narwhals.dependencies import is_pyarrow_table
+
+__all__ = [
+    "get_polars",
+    "get_pandas",
+    "get_modin",
+    "get_cudf",
+    "get_pyarrow",
+    "get_numpy",
+    "get_ibis",
+    "is_ibis_table",
+    "is_pandas_dataframe",
+    "is_pandas_series",
+    "is_pandas_index",
+    "is_polars_dataframe",
+    "is_polars_lazyframe",
+    "is_polars_series",
+    "is_modin_dataframe",
+    "is_modin_series",
+    "is_cudf_dataframe",
+    "is_cudf_series",
+    "is_pyarrow_table",
+    "is_pyarrow_chunked_array",
+    "is_numpy_array",
+    "is_dask_dataframe",
+    "is_pandas_like_dataframe",
+    "is_pandas_like_series",
+    "is_into_dataframe",
+    "is_into_series",
+]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 922d69e9..68e03449 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,9 +9,9 @@ import polars as pl
 import pyarrow as pa
 import pytest
 
-from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_dask_dataframe
-from narwhals.dependencies import get_modin
+from narwhals.stable.v1.dependencies import get_cudf
+from narwhals.stable.v1.dependencies import get_modin
 from tests.utils import PANDAS_VERSION
 
 if TYPE_CHECKING:
diff --git a/tests/dependencies/is_into_dataframe_test.py b/tests/dependencies/is_into_dataframe_test.py
index 77f5e7d4..95e54214 100644
--- a/tests/dependencies/is_into_dataframe_test.py
+++ b/tests/dependencies/is_into_dataframe_test.py
@@ -9,7 +9,7 @@ import polars as pl
 import pyarrow as pa
 
 import narwhals as nw
-from narwhals.dependencies import is_into_dataframe
+from narwhals.stable.v1.dependencies import is_into_dataframe
 
 if TYPE_CHECKING:
     from typing_extensions import Self
diff --git a/tests/dependencies/is_into_series_test.py b/tests/dependencies/is_into_series_test.py
index a4d4a827..8f8554b3 100644
--- a/tests/dependencies/is_into_series_test.py
+++ b/tests/dependencies/is_into_series_test.py
@@ -9,7 +9,7 @@ import polars as pl
 import pyarrow as pa
 
 import narwhals as nw
-from narwhals.dependencies import is_into_series
+from narwhals.stable.v1.dependencies import is_into_series
 
 if TYPE_CHECKING:
     from typing_extensions import Self
diff --git a/tests/dependencies/is_pandas_dataframe_test.py b/tests/dependencies/is_pandas_dataframe_test.py
index 96b87495..7c0c99f7 100644
--- a/tests/dependencies/is_pandas_dataframe_test.py
+++ b/tests/dependencies/is_pandas_dataframe_test.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import pandas as pd
 import polars as pl
 
-from narwhals.dependencies import is_pandas_dataframe
+from narwhals.stable.v1.dependencies import is_pandas_dataframe
 
 
 def test_is_pandas_dataframe() -> None:
diff --git a/tests/dependencies/is_pandas_index_test.py b/tests/dependencies/is_pandas_index_test.py
index 1d97dd82..2ff379f7 100644
--- a/tests/dependencies/is_pandas_index_test.py
+++ b/tests/dependencies/is_pandas_index_test.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import pandas as pd
 
-from narwhals.dependencies import is_pandas_index
+from narwhals.stable.v1.dependencies import is_pandas_index
 
 
 def test_is_pandas_index() -> None:
diff --git a/tests/translate/to_py_scalar_test.py b/tests/translate/to_py_scalar_test.py
index b13ba009..e1c4bbdf 100644
--- a/tests/translate/to_py_scalar_test.py
+++ b/tests/translate/to_py_scalar_test.py
@@ -9,7 +9,7 @@ import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
-from narwhals.dependencies import get_cudf
+from narwhals.stable.v1.dependencies import get_cudf
 from tests.utils import PANDAS_VERSION