diff --git a/narwhals/functions.py b/narwhals/functions.py index b5f10927b6..91a782c62e 100644 --- a/narwhals/functions.py +++ b/narwhals/functions.py @@ -61,6 +61,7 @@ from narwhals.typing import IntoSeriesT from narwhals.typing import NativeFrame from narwhals.typing import NativeLazyFrame + from narwhals.typing import NativeSeries from narwhals.typing import _2DArray _IntoSchema: TypeAlias = "Mapping[str, DType] | Schema | Sequence[str] | None" @@ -240,7 +241,7 @@ def _new_series_impl( else: # pragma: no cover native_namespace = implementation.to_native_namespace() try: - native_series = native_namespace.new_series(name, values, dtype) + native_series: NativeSeries = native_namespace.new_series(name, values, dtype) return from_native(native_series, series_only=True).alias(name) except AttributeError as e: msg = "Unknown namespace is expected to implement `new_series` constructor." @@ -325,7 +326,7 @@ def _from_dict_impl( try: # implementation is UNKNOWN, Narwhals extension using this feature should # implement `from_dict` function in the top-level namespace. - native_frame = native_namespace.from_dict(data, schema=schema) + native_frame: NativeFrame = native_namespace.from_dict(data, schema=schema) except AttributeError as e: msg = "Unknown namespace is expected to implement `from_dict` function." raise AttributeError(msg) from e @@ -441,7 +442,7 @@ def _from_numpy_impl( try: # implementation is UNKNOWN, Narwhals extension using this feature should # implement `from_numpy` function in the top-level namespace. - native_frame = native_namespace.from_numpy(data, schema=schema) + native_frame: NativeFrame = native_namespace.from_numpy(data, schema=schema) except AttributeError as e: msg = "Unknown namespace is expected to implement `from_numpy` function." raise AttributeError(msg) from e @@ -529,7 +530,7 @@ def _from_arrow_impl( try: # implementation is UNKNOWN, Narwhals extension using this feature should # implement PyCapsule support - native_frame = native_namespace.DataFrame(data) + native_frame: NativeFrame = native_namespace.DataFrame(data) except AttributeError as e: msg = "Unknown namespace is expected to implement `DataFrame` class which accepts object which supports PyCapsule Interface." raise AttributeError(msg) from e diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 79712278f4..fa4f937ca9 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -100,6 +100,7 @@ from narwhals.typing import _2DArray FrameT = TypeVar("FrameT", "DataFrame[Any]", "LazyFrame[Any]") + SeriesT = TypeVar("SeriesT", bound="Series[Any]") IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries", default=Any) T = TypeVar("T", default=Any) else: @@ -1154,6 +1155,10 @@ def _stableify( return obj +@overload +def from_native(native_object: SeriesT, **kwds: Any) -> SeriesT: ... + + @overload def from_native( native_object: IntoDataFrameT | IntoSeriesT, @@ -1540,7 +1545,7 @@ def from_native( ) -> Any: ... -def from_native( +def from_native( # noqa: D417 native_object: IntoFrameT | IntoFrame | IntoSeriesT | IntoSeries | T, *, strict: bool | None = None, @@ -1549,6 +1554,7 @@ def from_native( eager_or_interchange_only: bool = False, series_only: bool = False, allow_series: bool | None = None, + **kwds: Any, ) -> LazyFrame[IntoFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T: """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. @@ -1608,6 +1614,9 @@ def from_native( pass_through = validate_strict_and_pass_though( strict, pass_through, pass_through_default=False, emit_deprecation_warning=False ) + if kwds: + msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}" + raise TypeError(msg) result = _from_native_impl( native_object, diff --git a/narwhals/translate.py b/narwhals/translate.py index c2106c815d..4c7ca1193e 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -50,6 +50,7 @@ from narwhals.typing import IntoLazyFrameT from narwhals.typing import IntoSeries from narwhals.typing import IntoSeriesT + from narwhals.typing import SeriesT T = TypeVar("T") @@ -128,6 +129,10 @@ def to_native( return narwhals_object +@overload +def from_native(native_object: SeriesT, **kwds: Any) -> SeriesT: ... + + @overload def from_native( native_object: IntoDataFrameT | IntoSeries, @@ -298,7 +303,7 @@ def from_native( ) -> Any: ... -def from_native( +def from_native( # noqa: D417 native_object: IntoLazyFrameT | IntoFrameT | IntoSeriesT | IntoFrame | IntoSeries | T, *, strict: bool | None = None, @@ -306,6 +311,7 @@ def from_native( eager_only: bool = False, series_only: bool = False, allow_series: bool | None = None, + **kwds: Any, ) -> LazyFrame[IntoLazyFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T: """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. @@ -351,6 +357,9 @@ def from_native( pass_through = validate_strict_and_pass_though( strict, pass_through, pass_through_default=False, emit_deprecation_warning=True ) + if kwds: + msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}" + raise TypeError(msg) return _from_native_impl( # type: ignore[no-any-return] native_object, diff --git a/narwhals/typing.py b/narwhals/typing.py index c2c9475409..6f481768a5 100644 --- a/narwhals/typing.py +++ b/narwhals/typing.py @@ -102,7 +102,7 @@ def __native_namespace__(self) -> ModuleType: ... ... return df.columns """ -IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"] +IntoSeries: TypeAlias = "NativeSeries" """Anything which can be converted to a Narwhals Series. Use this if your function can accept an object which can be converted to `nw.Series` @@ -176,6 +176,7 @@ def __native_namespace__(self) -> ModuleType: ... """ LazyFrameT = TypeVar("LazyFrameT", bound="LazyFrame[Any]") +SeriesT = TypeVar("SeriesT", bound="Series[Any]") IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries") """TypeVar bound to object convertible to Narwhals Series. diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 1ef2805a31..9a67c10bc1 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -5,6 +5,7 @@ from importlib.util import find_spec from typing import TYPE_CHECKING from typing import Any +from typing import Callable from typing import Iterable from typing import Literal from typing import cast @@ -18,6 +19,7 @@ if TYPE_CHECKING: from typing_extensions import Self + from typing_extensions import assert_type from narwhals.utils import Version @@ -360,10 +362,63 @@ def test_from_native_lazyframe() -> None: stable_lazy = nw.from_native(lf_pl) unstable_lazy = unstable_nw.from_native(lf_pl) if TYPE_CHECKING: - from typing_extensions import assert_type - assert_type(stable_lazy, nw.LazyFrame[pl.LazyFrame]) assert_type(unstable_lazy, unstable_nw.LazyFrame[pl.LazyFrame]) assert isinstance(stable_lazy, nw.LazyFrame) assert isinstance(unstable_lazy, unstable_nw.LazyFrame) + + +def test_series_recursive() -> None: + """https://github.com/narwhals-dev/narwhals/issues/2239.""" + pytest.importorskip("polars") + import polars as pl + + pl_series = pl.Series(name="test", values=[1, 2, 3]) + nw_series = unstable_nw.from_native(pl_series, series_only=True) + with pytest.raises(AssertionError): + unstable_nw.Series(nw_series, level="full") + + nw_series_early_return = unstable_nw.from_native(nw_series, series_only=True) + + if TYPE_CHECKING: + assert_type(pl_series, pl.Series) + assert_type(nw_series, unstable_nw.Series[pl.Series]) + + nw_series_depth_2 = unstable_nw.Series(nw_series, level="full") # type: ignore[var-annotated] + # NOTE: Checking that the type is `Series[Unknown]` + assert_type(nw_series_depth_2, unstable_nw.Series) # type: ignore[type-arg] + assert_type(nw_series_early_return, unstable_nw.Series[pl.Series]) + + +def test_series_recursive_v1() -> None: + """https://github.com/narwhals-dev/narwhals/issues/2239.""" + pytest.importorskip("polars") + import polars as pl + + pl_series = pl.Series(name="test", values=[1, 2, 3]) + nw_series = nw.from_native(pl_series, series_only=True) + with pytest.raises(AssertionError): + nw.Series(nw_series, level="full") + + nw_series_early_return = nw.from_native(nw_series, series_only=True) + + if TYPE_CHECKING: + assert_type(pl_series, pl.Series) + assert_type(nw_series, nw.Series[pl.Series]) + + nw_series_depth_2 = nw.Series(nw_series, level="full") + # NOTE: `Unknown` isn't possible for `v1`, as it has a `TypeVar` default + assert_type(nw_series_depth_2, nw.Series[Any]) + assert_type(nw_series_early_return, nw.Series[pl.Series]) + + +@pytest.mark.parametrize("from_native", [unstable_nw.from_native, nw.from_native]) +def test_from_native_invalid_keywords(from_native: Callable[..., Any]) -> None: + pattern = r"from_native.+unexpected.+keyword.+bad_1" + + with pytest.raises(TypeError, match=pattern): + from_native(data, bad_1="invalid") + + with pytest.raises(TypeError, match=pattern): + from_native(data, bad_1="invalid", bad_2="also invalid")