Skip to content

Commit e565223

Browse files
authored
fix(typing): Narrow TypeVar used in Series (#2347)
1 parent ca8d63f commit e565223

File tree

5 files changed

+84
-9
lines changed

5 files changed

+84
-9
lines changed

narwhals/functions.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from narwhals.typing import IntoSeriesT
6262
from narwhals.typing import NativeFrame
6363
from narwhals.typing import NativeLazyFrame
64+
from narwhals.typing import NativeSeries
6465
from narwhals.typing import _2DArray
6566

6667
_IntoSchema: TypeAlias = "Mapping[str, DType] | Schema | Sequence[str] | None"
@@ -240,7 +241,7 @@ def _new_series_impl(
240241
else: # pragma: no cover
241242
native_namespace = implementation.to_native_namespace()
242243
try:
243-
native_series = native_namespace.new_series(name, values, dtype)
244+
native_series: NativeSeries = native_namespace.new_series(name, values, dtype)
244245
return from_native(native_series, series_only=True).alias(name)
245246
except AttributeError as e:
246247
msg = "Unknown namespace is expected to implement `new_series` constructor."
@@ -325,7 +326,7 @@ def _from_dict_impl(
325326
try:
326327
# implementation is UNKNOWN, Narwhals extension using this feature should
327328
# implement `from_dict` function in the top-level namespace.
328-
native_frame = native_namespace.from_dict(data, schema=schema)
329+
native_frame: NativeFrame = native_namespace.from_dict(data, schema=schema)
329330
except AttributeError as e:
330331
msg = "Unknown namespace is expected to implement `from_dict` function."
331332
raise AttributeError(msg) from e
@@ -441,7 +442,7 @@ def _from_numpy_impl(
441442
try:
442443
# implementation is UNKNOWN, Narwhals extension using this feature should
443444
# implement `from_numpy` function in the top-level namespace.
444-
native_frame = native_namespace.from_numpy(data, schema=schema)
445+
native_frame: NativeFrame = native_namespace.from_numpy(data, schema=schema)
445446
except AttributeError as e:
446447
msg = "Unknown namespace is expected to implement `from_numpy` function."
447448
raise AttributeError(msg) from e
@@ -529,7 +530,7 @@ def _from_arrow_impl(
529530
try:
530531
# implementation is UNKNOWN, Narwhals extension using this feature should
531532
# implement PyCapsule support
532-
native_frame = native_namespace.DataFrame(data)
533+
native_frame: NativeFrame = native_namespace.DataFrame(data)
533534
except AttributeError as e:
534535
msg = "Unknown namespace is expected to implement `DataFrame` class which accepts object which supports PyCapsule Interface."
535536
raise AttributeError(msg) from e

narwhals/stable/v1/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
from narwhals.typing import _2DArray
101101

102102
FrameT = TypeVar("FrameT", "DataFrame[Any]", "LazyFrame[Any]")
103+
SeriesT = TypeVar("SeriesT", bound="Series[Any]")
103104
IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries", default=Any)
104105
T = TypeVar("T", default=Any)
105106
else:
@@ -1154,6 +1155,10 @@ def _stableify(
11541155
return obj
11551156

11561157

1158+
@overload
1159+
def from_native(native_object: SeriesT, **kwds: Any) -> SeriesT: ...
1160+
1161+
11571162
@overload
11581163
def from_native(
11591164
native_object: IntoDataFrameT | IntoSeriesT,
@@ -1540,7 +1545,7 @@ def from_native(
15401545
) -> Any: ...
15411546

15421547

1543-
def from_native(
1548+
def from_native( # noqa: D417
15441549
native_object: IntoFrameT | IntoFrame | IntoSeriesT | IntoSeries | T,
15451550
*,
15461551
strict: bool | None = None,
@@ -1549,6 +1554,7 @@ def from_native(
15491554
eager_or_interchange_only: bool = False,
15501555
series_only: bool = False,
15511556
allow_series: bool | None = None,
1557+
**kwds: Any,
15521558
) -> LazyFrame[IntoFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T:
15531559
"""Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series.
15541560
@@ -1608,6 +1614,9 @@ def from_native(
16081614
pass_through = validate_strict_and_pass_though(
16091615
strict, pass_through, pass_through_default=False, emit_deprecation_warning=False
16101616
)
1617+
if kwds:
1618+
msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}"
1619+
raise TypeError(msg)
16111620

16121621
result = _from_native_impl(
16131622
native_object,

narwhals/translate.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from narwhals.typing import IntoLazyFrameT
5151
from narwhals.typing import IntoSeries
5252
from narwhals.typing import IntoSeriesT
53+
from narwhals.typing import SeriesT
5354

5455
T = TypeVar("T")
5556

@@ -128,6 +129,10 @@ def to_native(
128129
return narwhals_object
129130

130131

132+
@overload
133+
def from_native(native_object: SeriesT, **kwds: Any) -> SeriesT: ...
134+
135+
131136
@overload
132137
def from_native(
133138
native_object: IntoDataFrameT | IntoSeries,
@@ -298,14 +303,15 @@ def from_native(
298303
) -> Any: ...
299304

300305

301-
def from_native(
306+
def from_native( # noqa: D417
302307
native_object: IntoLazyFrameT | IntoFrameT | IntoSeriesT | IntoFrame | IntoSeries | T,
303308
*,
304309
strict: bool | None = None,
305310
pass_through: bool | None = None,
306311
eager_only: bool = False,
307312
series_only: bool = False,
308313
allow_series: bool | None = None,
314+
**kwds: Any,
309315
) -> LazyFrame[IntoLazyFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T:
310316
"""Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series.
311317
@@ -351,6 +357,9 @@ def from_native(
351357
pass_through = validate_strict_and_pass_though(
352358
strict, pass_through, pass_through_default=False, emit_deprecation_warning=True
353359
)
360+
if kwds:
361+
msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}"
362+
raise TypeError(msg)
354363

355364
return _from_native_impl( # type: ignore[no-any-return]
356365
native_object,

narwhals/typing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __native_namespace__(self) -> ModuleType: ...
102102
... return df.columns
103103
"""
104104

105-
IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"]
105+
IntoSeries: TypeAlias = "NativeSeries"
106106
"""Anything which can be converted to a Narwhals Series.
107107
108108
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: ...
176176
"""
177177

178178
LazyFrameT = TypeVar("LazyFrameT", bound="LazyFrame[Any]")
179+
SeriesT = TypeVar("SeriesT", bound="Series[Any]")
179180

180181
IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries")
181182
"""TypeVar bound to object convertible to Narwhals Series.

tests/translate/from_native_test.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from importlib.util import find_spec
66
from typing import TYPE_CHECKING
77
from typing import Any
8+
from typing import Callable
89
from typing import Iterable
910
from typing import Literal
1011
from typing import cast
@@ -18,6 +19,7 @@
1819

1920
if TYPE_CHECKING:
2021
from typing_extensions import Self
22+
from typing_extensions import assert_type
2123

2224
from narwhals.utils import Version
2325

@@ -360,10 +362,63 @@ def test_from_native_lazyframe() -> None:
360362
stable_lazy = nw.from_native(lf_pl)
361363
unstable_lazy = unstable_nw.from_native(lf_pl)
362364
if TYPE_CHECKING:
363-
from typing_extensions import assert_type
364-
365365
assert_type(stable_lazy, nw.LazyFrame[pl.LazyFrame])
366366
assert_type(unstable_lazy, unstable_nw.LazyFrame[pl.LazyFrame])
367367

368368
assert isinstance(stable_lazy, nw.LazyFrame)
369369
assert isinstance(unstable_lazy, unstable_nw.LazyFrame)
370+
371+
372+
def test_series_recursive() -> None:
373+
"""https://github.com/narwhals-dev/narwhals/issues/2239."""
374+
pytest.importorskip("polars")
375+
import polars as pl
376+
377+
pl_series = pl.Series(name="test", values=[1, 2, 3])
378+
nw_series = unstable_nw.from_native(pl_series, series_only=True)
379+
with pytest.raises(AssertionError):
380+
unstable_nw.Series(nw_series, level="full")
381+
382+
nw_series_early_return = unstable_nw.from_native(nw_series, series_only=True)
383+
384+
if TYPE_CHECKING:
385+
assert_type(pl_series, pl.Series)
386+
assert_type(nw_series, unstable_nw.Series[pl.Series])
387+
388+
nw_series_depth_2 = unstable_nw.Series(nw_series, level="full") # type: ignore[var-annotated]
389+
# NOTE: Checking that the type is `Series[Unknown]`
390+
assert_type(nw_series_depth_2, unstable_nw.Series) # type: ignore[type-arg]
391+
assert_type(nw_series_early_return, unstable_nw.Series[pl.Series])
392+
393+
394+
def test_series_recursive_v1() -> None:
395+
"""https://github.com/narwhals-dev/narwhals/issues/2239."""
396+
pytest.importorskip("polars")
397+
import polars as pl
398+
399+
pl_series = pl.Series(name="test", values=[1, 2, 3])
400+
nw_series = nw.from_native(pl_series, series_only=True)
401+
with pytest.raises(AssertionError):
402+
nw.Series(nw_series, level="full")
403+
404+
nw_series_early_return = nw.from_native(nw_series, series_only=True)
405+
406+
if TYPE_CHECKING:
407+
assert_type(pl_series, pl.Series)
408+
assert_type(nw_series, nw.Series[pl.Series])
409+
410+
nw_series_depth_2 = nw.Series(nw_series, level="full")
411+
# NOTE: `Unknown` isn't possible for `v1`, as it has a `TypeVar` default
412+
assert_type(nw_series_depth_2, nw.Series[Any])
413+
assert_type(nw_series_early_return, nw.Series[pl.Series])
414+
415+
416+
@pytest.mark.parametrize("from_native", [unstable_nw.from_native, nw.from_native])
417+
def test_from_native_invalid_keywords(from_native: Callable[..., Any]) -> None:
418+
pattern = r"from_native.+unexpected.+keyword.+bad_1"
419+
420+
with pytest.raises(TypeError, match=pattern):
421+
from_native(data, bad_1="invalid")
422+
423+
with pytest.raises(TypeError, match=pattern):
424+
from_native(data, bad_1="invalid", bad_2="also invalid")

0 commit comments

Comments
 (0)