diff --git a/docs/api-reference/dataframe.md b/docs/api-reference/dataframe.md index 447acbc15..7b146eeac 100644 --- a/docs/api-reference/dataframe.md +++ b/docs/api-reference/dataframe.md @@ -24,6 +24,7 @@ - join - join_asof - lazy + - native - null_count - pipe - rename diff --git a/docs/api-reference/lazyframe.md b/docs/api-reference/lazyframe.md index a6776e08c..c6d3e2368 100644 --- a/docs/api-reference/lazyframe.md +++ b/docs/api-reference/lazyframe.md @@ -18,6 +18,7 @@ - join_asof - lazy - pipe + - native - rename - schema - select diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md index e8572dda8..13b69f97d 100644 --- a/docs/api-reference/series.md +++ b/docs/api-reference/series.md @@ -39,6 +39,7 @@ - min - mode - name + - native - n_unique - null_count - pipe diff --git a/docs/basics/dataframe_conversion.md b/docs/basics/dataframe_conversion.md index 690f5d093..d07d37615 100644 --- a/docs/basics/dataframe_conversion.md +++ b/docs/basics/dataframe_conversion.md @@ -49,7 +49,7 @@ Similarly, if your library uses Polars internally, you can convert any user-supp ```python exec="1" source="above" session="conversion" result="python" def df_to_polars(df: IntoDataFrame) -> pl.DataFrame: - return nw.from_arrow(nw.from_native(df), native_namespace=pl).to_native() + return nw.from_arrow(nw.from_native(df), native_namespace=pl).native print(df_to_polars(df_duckdb)) # You can only execute this line of code once. diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 3ddaa2814..6bada59f7 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -11,11 +11,11 @@ from typing import Sequence from typing import TypeVar from typing import overload +from warnings import warn from narwhals.dependencies import get_polars from narwhals.dependencies import is_numpy_array from narwhals.schema import Schema -from narwhals.translate import to_native from narwhals.utils import flatten from narwhals.utils import is_sequence_but_not_str from narwhals.utils import parse_version @@ -328,6 +328,54 @@ def _series(self) -> type[Series]: def _lazyframe(self) -> type[LazyFrame[Any]]: return LazyFrame + @property + def native(self: Self) -> DataFrameT: + """ + Convert Narwhals DataFrame to native one. + + Returns: + Object of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import pyarrow as pa + >>> import narwhals as nw + >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]} + >>> df_pd = pd.DataFrame(data) + >>> df_pl = pl.DataFrame(data) + >>> df_pa = pa.table(data) + + Calling `.native` on a Narwhals DataFrame returns the native object: + + >>> nw.from_native(df_pd).native + foo bar ham + 0 1 6.0 a + 1 2 7.0 b + 2 3 8.0 c + >>> nw.from_native(df_pl).native + shape: (3, 3) + ┌─────┬─────┬─────┐ + │ foo ┆ bar ┆ ham │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ f64 ┆ str │ + ╞═════╪═════╪═════╡ + │ 1 ┆ 6.0 ┆ a │ + │ 2 ┆ 7.0 ┆ b │ + │ 3 ┆ 8.0 ┆ c │ + └─────┴─────┴─────┘ + >>> nw.from_native(df_pa).native + pyarrow.Table + foo: int64 + bar: double + ham: string + ---- + foo: [[1,2,3]] + bar: [[6,7,8]] + ham: [["a","b","c"]] + """ + return self._compliant_frame._native_frame # type: ignore[no-any-return] + def __init__( self, df: Any, @@ -430,7 +478,7 @@ def lazy(self) -> LazyFrame[Any]: """ return self._lazyframe(self._compliant_frame.lazy(), level=self._level) - def to_native(self) -> DataFrameT: + def to_native(self: Self) -> DataFrameT: """ Convert Narwhals DataFrame to native one. @@ -449,12 +497,12 @@ def to_native(self) -> DataFrameT: Calling `to_native` on a Narwhals DataFrame returns the native object: - >>> nw.from_native(df_pd).to_native() + >>> nw.from_native(df_pd).to_native() # doctest:+SKIP foo bar ham 0 1 6.0 a 1 2 7.0 b 2 3 8.0 c - >>> nw.from_native(df_pl).to_native() + >>> nw.from_native(df_pl).to_native() # doctest:+SKIP shape: (3, 3) ┌─────┬─────┬─────┐ │ foo ┆ bar ┆ ham │ @@ -465,7 +513,7 @@ def to_native(self) -> DataFrameT: │ 2 ┆ 7.0 ┆ b │ │ 3 ┆ 8.0 ┆ c │ └─────┴─────┴─────┘ - >>> nw.from_native(df_pa).to_native() + >>> nw.from_native(df_pa).to_native() # doctest:+SKIP pyarrow.Table foo: int64 bar: double @@ -475,8 +523,13 @@ def to_native(self) -> DataFrameT: bar: [[6,7,8]] ham: [["a","b","c"]] """ - - return self._compliant_frame._native_frame # type: ignore[no-any-return] + warn( + "Use `.native` property instead. `.to_native()` is " + "deprecated and it will be removed in future versions", + DeprecationWarning, + stacklevel=2, + ) + return self.native def to_pandas(self) -> pd.DataFrame: """ @@ -2765,6 +2818,45 @@ class LazyFrame(BaseFrame[FrameT]): def _dataframe(self) -> type[DataFrame[Any]]: return DataFrame + @property + def native(self: Self) -> FrameT: + """ + Convert Narwhals LazyFrame to native one. + + Returns: + Object of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import pyarrow as pa + >>> import narwhals as nw + >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]} + >>> df_pd = pd.DataFrame(data) + >>> df_pl = pl.LazyFrame(data) + >>> df_pa = pa.table(data) + + Calling `.native` on a Narwhals DataFrame returns the native object: + + >>> nw.from_native(df_pd).lazy().native + foo bar ham + 0 1 6.0 a + 1 2 7.0 b + 2 3 8.0 c + >>> nw.from_native(df_pl).native.collect() + shape: (3, 3) + ┌─────┬─────┬─────┐ + │ foo ┆ bar ┆ ham │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ f64 ┆ str │ + ╞═════╪═════╪═════╡ + │ 1 ┆ 6.0 ┆ a │ + │ 2 ┆ 7.0 ┆ b │ + │ 3 ┆ 8.0 ┆ c │ + └─────┴─────┴─────┘ + """ + return self._compliant_frame._native_frame # type: ignore[no-any-return] + def __init__( self, df: Any, @@ -2820,7 +2912,7 @@ def collect(self) -> DataFrame[Any]: | Use `.to_native` to see native output | └───────────────────────────────────────┘ >>> df = lf.group_by("a").agg(nw.all().sum()).collect() - >>> df.to_native().sort("a") + >>> df.native.sort("a") shape: (3, 3) ┌─────┬─────┬─────┐ │ a ┆ b ┆ c │ @@ -2837,7 +2929,7 @@ def collect(self) -> DataFrame[Any]: level=self._level, ) - def to_native(self) -> FrameT: + def to_native(self: Self) -> FrameT: """ Convert Narwhals LazyFrame to native one. @@ -2856,12 +2948,12 @@ def to_native(self) -> FrameT: Calling `to_native` on a Narwhals DataFrame returns the native object: - >>> nw.from_native(df_pd).lazy().to_native() + >>> nw.from_native(df_pd).lazy().to_native() # doctest:+SKIP foo bar ham 0 1 6.0 a 1 2 7.0 b 2 3 8.0 c - >>> nw.from_native(df_pl).to_native().collect() + >>> nw.from_native(df_pl).to_native().collect() # doctest:+SKIP shape: (3, 3) ┌─────┬─────┬─────┐ │ foo ┆ bar ┆ ham │ @@ -2873,8 +2965,13 @@ def to_native(self) -> FrameT: │ 3 ┆ 8.0 ┆ c │ └─────┴─────┴─────┘ """ - - return to_native(narwhals_object=self, strict=True) + warn( + "Use `.native` property instead. `.to_native()` is " + "deprecated and it will be removed in future versions", + DeprecationWarning, + stacklevel=2, + ) + return self.native # inherited def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Self: diff --git a/narwhals/series.py b/narwhals/series.py index 6f5223202..f1045c02b 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -9,6 +9,7 @@ from typing import Sequence from typing import TypeVar from typing import overload +from warnings import warn from narwhals.utils import parse_version @@ -41,6 +42,46 @@ def _dataframe(self) -> type[DataFrame[Any]]: return DataFrame + @property + def native(self: Self) -> Any: + """ + Convert Narwhals series to native series. + + Returns: + Series of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import narwhals as nw + >>> s = [1, 2, 3] + >>> s_pd = pd.Series(s) + >>> s_pl = pl.Series(s) + + We define a library agnostic function: + + >>> @nw.narwhalify + ... def func(s): + ... return s.native + + We can then pass either pandas or Polars to `func`: + + >>> func(s_pd) + 0 1 + 1 2 + 2 3 + dtype: int64 + >>> func(s_pl) # doctest: +NORMALIZE_WHITESPACE + shape: (3,) + Series: '' [i64] + [ + 1 + 2 + 3 + ] + """ + return self._compliant_series._native_series + def __init__( self: Self, series: Any, @@ -97,7 +138,7 @@ def __arrow_c_stream__(self, requested_schema: object | None = None) -> object: ca = pa.chunked_array([self.to_arrow()]) return ca.__arrow_c_stream__(requested_schema=requested_schema) - def to_native(self) -> Any: + def to_native(self: Self) -> Any: """ Convert Narwhals series to native series. @@ -120,12 +161,12 @@ def to_native(self) -> Any: We can then pass either pandas or Polars to `func`: - >>> func(s_pd) + >>> func(s_pd) # doctest:+SKIP 0 1 1 2 2 3 dtype: int64 - >>> func(s_pl) # doctest: +NORMALIZE_WHITESPACE + >>> func(s_pl) # doctest:+SKIP shape: (3,) Series: '' [i64] [ @@ -134,7 +175,13 @@ def to_native(self) -> Any: 3 ] """ - return self._compliant_series._native_series + warn( + "Use `.native` property instead. `.to_native()` is " + "deprecated and it will be removed in future versions", + DeprecationWarning, + stacklevel=2, + ) + return self.native def scatter(self, indices: int | Sequence[int], values: Any) -> Self: """ diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 68aca7706..e0f4dcb0a 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -326,6 +326,53 @@ def is_unique(self: Self) -> Series: """ return super().is_unique() # type: ignore[return-value] + def to_native(self: Self) -> IntoDataFrameT: + """ + Convert Narwhals DataFrame to native one. + + Returns: + Object of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import pyarrow as pa + >>> import narwhals as nw + >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]} + >>> df_pd = pd.DataFrame(data) + >>> df_pl = pl.DataFrame(data) + >>> df_pa = pa.table(data) + + Calling `to_native` on a Narwhals DataFrame returns the native object: + + >>> nw.from_native(df_pd).to_native() # doctest:+SKIP + foo bar ham + 0 1 6.0 a + 1 2 7.0 b + 2 3 8.0 c + >>> nw.from_native(df_pl).to_native() # doctest:+SKIP + shape: (3, 3) + ┌─────┬─────┬─────┐ + │ foo ┆ bar ┆ ham │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ f64 ┆ str │ + ╞═════╪═════╪═════╡ + │ 1 ┆ 6.0 ┆ a │ + │ 2 ┆ 7.0 ┆ b │ + │ 3 ┆ 8.0 ┆ c │ + └─────┴─────┴─────┘ + >>> nw.from_native(df_pa).to_native() # doctest:+SKIP + pyarrow.Table + foo: int64 + bar: double + ham: string + ---- + foo: [[1,2,3]] + bar: [[6,7,8]] + ham: [["a","b","c"]] + """ + return self.native + def _l1_norm(self: Self) -> Self: """Private, just used to test the stable API.""" return self.select(all()._l1_norm()) @@ -369,7 +416,7 @@ def collect(self) -> DataFrame[Any]: | Use `.to_native` to see native output | └───────────────────────────────────────┘ >>> df = lf.group_by("a").agg(nw.all().sum()).collect() - >>> df.to_native().sort("a") + >>> df.native.sort("a") shape: (3, 3) ┌─────┬─────┬─────┐ │ a ┆ b ┆ c │ @@ -383,6 +430,44 @@ def collect(self) -> DataFrame[Any]: """ return super().collect() # type: ignore[return-value] + def to_native(self: Self) -> IntoFrameT: + """ + Convert Narwhals LazyFrame to native one. + + Returns: + Object of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import pyarrow as pa + >>> import narwhals as nw + >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]} + >>> df_pd = pd.DataFrame(data) + >>> df_pl = pl.LazyFrame(data) + >>> df_pa = pa.table(data) + + Calling `to_native` on a Narwhals DataFrame returns the native object: + + >>> nw.from_native(df_pd).lazy().to_native() # doctest:+SKIP + foo bar ham + 0 1 6.0 a + 1 2 7.0 b + 2 3 8.0 c + >>> nw.from_native(df_pl).to_native().collect() # doctest:+SKIP + shape: (3, 3) + ┌─────┬─────┬─────┐ + │ foo ┆ bar ┆ ham │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ f64 ┆ str │ + ╞═════╪═════╪═════╡ + │ 1 ┆ 6.0 ┆ a │ + │ 2 ┆ 7.0 ┆ b │ + │ 3 ┆ 8.0 ┆ c │ + └─────┴─────┴─────┘ + """ + return self.native + def _l1_norm(self: Self) -> Self: """Private, just used to test the stable API.""" return self.select(all()._l1_norm()) @@ -501,6 +586,45 @@ def value_counts( sort=sort, parallel=parallel, name=name, normalize=normalize ) + def to_native(self: Self) -> Any: + """ + Convert Narwhals series to native series. + + Returns: + Series of class that user started with. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import narwhals as nw + >>> s = [1, 2, 3] + >>> s_pd = pd.Series(s) + >>> s_pl = pl.Series(s) + + We define a library agnostic function: + + >>> @nw.narwhalify + ... def func(s): + ... return s.to_native() + + We can then pass either pandas or Polars to `func`: + + >>> func(s_pd) # doctest:+SKIP + 0 1 + 1 2 + 2 3 + dtype: int64 + >>> func(s_pl) # doctest:+SKIP + shape: (3,) + Series: '' [i64] + [ + 1 + 2 + 3 + ] + """ + return self.native + class Expr(NwExpr): def _l1_norm(self) -> Self: diff --git a/tests/frame/to_native_test.py b/tests/frame/to_native_test.py index fb90caf10..4a7a2bae9 100644 --- a/tests/frame/to_native_test.py +++ b/tests/frame/to_native_test.py @@ -2,6 +2,9 @@ from typing import TYPE_CHECKING +import pytest + +import narwhals as nw_unstable import narwhals.stable.v1 as nw if TYPE_CHECKING: @@ -14,3 +17,13 @@ def test_to_native(constructor: Constructor) -> None: df = nw.from_native(df_raw) assert isinstance(df.to_native(), df_raw.__class__) + assert isinstance(df.native, df_raw.__class__) + + +def test_raise_warning(constructor: Constructor) -> None: + data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]} + df_raw = constructor(data) + df = nw_unstable.from_native(df_raw) + + with pytest.deprecated_call(): + assert isinstance(df.to_native(), df_raw.__class__) diff --git a/tests/from_pycapsule_test.py b/tests/from_pycapsule_test.py index 7d91a44f3..b48db2c36 100644 --- a/tests/from_pycapsule_test.py +++ b/tests/from_pycapsule_test.py @@ -16,7 +16,7 @@ def test_from_arrow_to_arrow() -> None: df = nw.from_native(pl.DataFrame({"ab": [1, 2, 3], "ba": [4, 5, 6]}), eager_only=True) result = nw.from_arrow(df, native_namespace=pa) - assert isinstance(result.to_native(), pa.Table) + assert isinstance(result.native, pa.Table) expected = {"ab": [1, 2, 3], "ba": [4, 5, 6]} assert_equal_data(result, expected) @@ -27,7 +27,7 @@ def test_from_arrow_to_polars(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delitem(sys.modules, "pandas") df = nw.from_native(tbl, eager_only=True) result = nw.from_arrow(df, native_namespace=pl) - assert isinstance(result.to_native(), pl.DataFrame) + assert isinstance(result.native, pl.DataFrame) expected = {"ab": [1, 2, 3], "ba": [4, 5, 6]} assert_equal_data(result, expected) assert "pandas" not in sys.modules @@ -37,7 +37,7 @@ def test_from_arrow_to_polars(monkeypatch: pytest.MonkeyPatch) -> None: def test_from_arrow_to_pandas() -> None: df = nw.from_native(pa.table({"ab": [1, 2, 3], "ba": [4, 5, 6]}), eager_only=True) result = nw.from_arrow(df, native_namespace=pd) - assert isinstance(result.to_native(), pd.DataFrame) + assert isinstance(result.native, pd.DataFrame) expected = {"ab": [1, 2, 3], "ba": [4, 5, 6]} assert_equal_data(result, expected) diff --git a/tests/series_only/to_native_test.py b/tests/series_only/to_native_test.py index e6955b4c3..9203d341f 100644 --- a/tests/series_only/to_native_test.py +++ b/tests/series_only/to_native_test.py @@ -2,6 +2,9 @@ from typing import TYPE_CHECKING +import pytest + +import narwhals as nw_unstable import narwhals.stable.v1 as nw if TYPE_CHECKING: @@ -13,5 +16,15 @@ def test_to_native(constructor_eager: ConstructorEager) -> None: orig_series = constructor_eager({"a": data})["a"] # type: ignore[index] nw_series = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"] - result = nw_series.to_native() - assert isinstance(result, orig_series.__class__) + assert isinstance(nw_series.to_native(), orig_series.__class__) + assert isinstance(nw_series.native, orig_series.__class__) + + +def test_raise_warning(constructor_eager: ConstructorEager) -> None: + orig_series = constructor_eager({"a": data})["a"] # type: ignore[index] + nw_series = nw_unstable.from_native(constructor_eager({"a": data}), eager_only=True)[ + "a" + ] + + with pytest.deprecated_call(): + assert isinstance(nw_series.to_native(), orig_series.__class__) diff --git a/tests/stable_api_test.py b/tests/stable_api_test.py index 414b20ad0..108c0a519 100644 --- a/tests/stable_api_test.py +++ b/tests/stable_api_test.py @@ -49,7 +49,7 @@ def test_renamed_taxicab_norm_dataframe(constructor: Constructor) -> None: def func(df_any: Any) -> Any: df = nw_v1.from_native(df_any) df = df._l1_norm() - return df.to_native() + return df.native result = nw_v1.from_native(func(constructor({"a": [1, 2, 3, -4, 5]}))) expected = {"a": [15]} diff --git a/tests/utils_test.py b/tests/utils_test.py index fb668b4d2..dcfbe4acd 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -102,7 +102,7 @@ def test_maybe_reset_index_pandas() -> None: result = nw.maybe_reset_index(pandas_df) expected = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) assert_frame_equal(nw.to_native(result), expected) - assert result.to_native() is pandas_df.to_native() + assert result.native is pandas_df.native pandas_series = nw.from_native( pd.Series([1, 2, 3], index=[7, 8, 9]), series_only=True ) @@ -113,7 +113,7 @@ def test_maybe_reset_index_pandas() -> None: result_s = nw.maybe_reset_index(pandas_series) expected_s = pd.Series([1, 2, 3]) assert_series_equal(nw.to_native(result_s), expected_s) - assert result_s.to_native() is pandas_series.to_native() + assert result_s.native is pandas_series.native def test_maybe_reset_index_polars() -> None: diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py index b7d8595aa..d9ef27678 100644 --- a/utils/check_api_reference.py +++ b/utils/check_api_reference.py @@ -19,6 +19,7 @@ "is_sorted", "item", "name", + "native", "rename", "scatter", "shape",