Repository: narwhals-dev/narwhals
Pull request #433 (2024-07-07 18:27:52)

chore: simplify instantiation of top-level Narwhals objects
<!--
# 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 # 
- 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.




Patch:
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 538c6a44..cd100882 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -78,7 +78,10 @@ class PandasGroupBy:
         )
 
     def __iter__(self) -> Iterator[tuple[Any, PandasDataFrame]]:
-        return self._grouped.__iter__()  # type: ignore[no-any-return]
+        yield from (
+            (key, self._from_native_dataframe(sub_df))
+            for (key, sub_df) in self._grouped.__iter__()
+        )
 
 
 def agg_pandas(  # noqa: PLR0913
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 33c542ed..1a3f4703 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -11,17 +11,10 @@ from typing import Sequence
 from typing import TypeVar
 from typing import overload
 
-from narwhals._arrow.dataframe import ArrowDataFrame
-from narwhals._pandas_like.dataframe import PandasDataFrame
-from narwhals.dependencies import get_cudf
-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.dtypes import to_narwhals_dtype
 from narwhals.utils import flatten
-from narwhals.utils import parse_version
 from narwhals.utils import validate_same_library
 
 if TYPE_CHECKING:
@@ -231,34 +224,8 @@ class DataFrame(BaseFrame[FrameT]):
             self._dataframe: Any = df.__narwhals_dataframe__()
         elif is_polars and isinstance(df, get_polars().DataFrame):
             self._dataframe = df
-        elif is_polars and isinstance(df, get_polars().LazyFrame):
-            raise TypeError(
-                "Can't instantiate DataFrame from Polars LazyFrame. Call `collect()` first, or use `narwhals.LazyFrame` if you don't specifically require eager execution."
-            )
-        elif (pd := get_pandas()) is not None and isinstance(df, pd.DataFrame):
-            self._dataframe = PandasDataFrame(
-                df, implementation="pandas", backend_version=parse_version(pd.__version__)
-            )
-        elif (mpd := get_modin()) is not None and isinstance(
-            df, mpd.DataFrame
-        ):  # pragma: no cover
-            self._dataframe = PandasDataFrame(
-                df, implementation="modin", backend_version=parse_version(mpd.__version__)
-            )
-        elif (cudf := get_cudf()) is not None and isinstance(
-            df, cudf.DataFrame
-        ):  # pragma: no cover
-            self._dataframe = PandasDataFrame(
-                df, implementation="cudf", backend_version=parse_version(cudf.__version__)
-            )
-        elif (pa := get_pyarrow()) is not None and isinstance(
-            df, pa.Table
-        ):  # pragma: no cover
-            self._dataframe = ArrowDataFrame(
-                df, backend_version=parse_version(pa.__version__)
-            )
         else:
-            msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(df)}"
+            msg = f"Expected polars DataFrame or object which implements `__narwhals_dataframe__`, got: {type(df)}"
             raise TypeError(msg)
 
     def __array__(self) -> np.ndarray:
@@ -1863,13 +1830,12 @@ class LazyFrame(BaseFrame[FrameT]):
         self._backend_version = backend_version
         if hasattr(df, "__narwhals_lazyframe__"):
             self._dataframe: Any = df.__narwhals_lazyframe__()
-        elif is_polars or (
+        elif is_polars and (
             (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame)
         ):
             self._dataframe = df
-            self._is_polars = True
         else:
-            msg = f"Expected Polars lazyframe or object that implements `__narwhals_lazyframe__`, got: {type(df)}"
+            msg = f"Expected Polars LazyFrame or object that implements `__narwhals_lazyframe__`, got: {type(df)}"
             raise TypeError(msg)
 
     def __repr__(self) -> str:  # pragma: no cover
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 008caca0..9914bab6 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -230,6 +230,10 @@ def from_native(  # noqa: PLR0915
     Returns:
         narwhals.DataFrame or narwhals.LazyFrame or narwhals.Series
     """
+    from narwhals._arrow.dataframe import ArrowDataFrame
+    from narwhals._arrow.series import ArrowSeries
+    from narwhals._pandas_like.dataframe import PandasDataFrame
+    from narwhals._pandas_like.series import PandasSeries
     from narwhals.dataframe import DataFrame
     from narwhals.dataframe import LazyFrame
     from narwhals.series import Series
@@ -254,14 +258,18 @@ def from_native(  # noqa: PLR0915
             raise TypeError("Cannot only use `eager_only` with polars.LazyFrame")
         return LazyFrame(
             native_dataframe,
-            is_polars=False,
+            is_polars=True,
             backend_version=parse_version(pl.__version__),
         )
     elif (pd := get_pandas()) is not None and isinstance(native_dataframe, pd.DataFrame):
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with dataframe")
         return DataFrame(
-            native_dataframe,
+            PandasDataFrame(
+                native_dataframe,
+                backend_version=parse_version(pd.__version__),
+                implementation="pandas",
+            ),
             is_polars=False,
             backend_version=parse_version(pd.__version__),
         )
@@ -271,7 +279,11 @@ def from_native(  # noqa: PLR0915
         if series_only:
             raise TypeError("Cannot only use `series_only` with modin.DataFrame")
         return DataFrame(
-            native_dataframe,
+            PandasDataFrame(
+                native_dataframe,
+                implementation="modin",
+                backend_version=parse_version(mpd.__version__),
+            ),
             is_polars=False,
             backend_version=parse_version(mpd.__version__),
         )
@@ -281,7 +293,11 @@ def from_native(  # noqa: PLR0915
         if series_only:
             raise TypeError("Cannot only use `series_only` with modin.DataFrame")
         return DataFrame(
-            native_dataframe,
+            PandasDataFrame(
+                native_dataframe,
+                implementation="cudf",
+                backend_version=parse_version(cudf.__version__),
+            ),
             is_polars=False,
             backend_version=parse_version(cudf.__version__),
         )
@@ -289,7 +305,9 @@ def from_native(  # noqa: PLR0915
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with arrow table")
         return DataFrame(
-            native_dataframe,
+            ArrowDataFrame(
+                native_dataframe, backend_version=parse_version(pa.__version__)
+            ),
             is_polars=False,
             backend_version=parse_version(pa.__version__),
         )
@@ -325,7 +343,11 @@ def from_native(  # noqa: PLR0915
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
         return Series(
-            native_dataframe,
+            PandasSeries(
+                native_dataframe,
+                implementation="pandas",
+                backend_version=parse_version(pd.__version__),
+            ),
             is_polars=False,
             backend_version=parse_version(pd.__version__),
         )
@@ -335,7 +357,11 @@ def from_native(  # noqa: PLR0915
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
         return Series(
-            native_dataframe,
+            PandasSeries(
+                native_dataframe,
+                implementation="modin",
+                backend_version=parse_version(mpd.__version__),
+            ),
             is_polars=False,
             backend_version=parse_version(mpd.__version__),
         )
@@ -345,7 +371,11 @@ def from_native(  # noqa: PLR0915
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
         return Series(
-            native_dataframe,
+            PandasSeries(
+                native_dataframe,
+                implementation="cudf",
+                backend_version=parse_version(cudf.__version__),
+            ),
             is_polars=False,
             backend_version=parse_version(cudf.__version__),
         )
@@ -355,7 +385,9 @@ def from_native(  # noqa: PLR0915
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
         return Series(
-            native_dataframe,
+            ArrowSeries(
+                native_dataframe, backend_version=parse_version(pa.__version__), name=""
+            ),
             is_polars=False,
             backend_version=parse_version(pa.__version__),
         )


Test patch:
diff --git a/tests/frame/instantiation_test.py b/tests/frame/instantiation_test.py
index fba23ea9..b37a6250 100644
--- a/tests/frame/instantiation_test.py
+++ b/tests/frame/instantiation_test.py
@@ -17,7 +17,8 @@ def test_lazy_instantiation() -> None:
 def test_lazy_instantiation_error() -> None:
     df_lazy = pl.LazyFrame({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]})
     with pytest.raises(
-        TypeError, match="Can't instantiate DataFrame from Polars LazyFrame."
+        TypeError,
+        match="Expected polars DataFrame or object which implements `__narwhals_dataframe__`",
     ):
         _ = nw.DataFrame(df_lazy, is_polars=True, backend_version=(0,)).shape
 
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index f8dfd5f5..f6d08393 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -240,12 +240,12 @@ def test_accepted_dataframes() -> None:
     array = np.array([[0, 4.0], [2, 5]])
     with pytest.raises(
         TypeError,
-        match="Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: <class 'numpy.ndarray'>",
+        match="Expected polars DataFrame or object which implements `__narwhals_dataframe__`",
     ):
         nw.DataFrame(array, is_polars=False, backend_version=(1,))
     with pytest.raises(
         TypeError,
-        match="Expected Polars lazyframe or object that implements `__narwhals_lazyframe__`, got: <class 'numpy.ndarray'>",
+        match="Expected Polars LazyFrame or object that implements `__narwhals_lazyframe__`, got: <class 'numpy.ndarray'>",
     ):
         nw.LazyFrame(array, is_polars=False, backend_version=(1,))