Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions narwhals/testing/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame:

if TYPE_CHECKING:
from collections.abc import Iterable
from types import ModuleType

import ibis
import pandas as pd
Expand All @@ -59,9 +60,16 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame:
from sqlframe.duckdb import DuckDBSession
from typing_extensions import Concatenate, TypeAlias

from narwhals import DataFrame, LazyFrame
from narwhals._native import NativeDask, NativeDuckDB, NativePySpark, NativeSQLFrame
from narwhals.testing.typing import Data
from narwhals.typing import IntoDataFrame, IntoFrame, IntoLazyFrame
from narwhals.typing import (
IntoDataFrame,
IntoDataFrameT,
IntoFrame,
IntoLazyFrame,
IntoLazyFrameT,
)


__all__ = (
Expand Down Expand Up @@ -97,6 +105,8 @@ class frame_constructor(Generic[T_co]): # noqa: N801

_registry: ClassVar[dict[str, frame_constructor[IntoFrame]]] = {}

func: Callable[Concatenate[Data, ...], T_co]

def __init__(
self,
func: Callable[Concatenate[Data, ...], T_co],
Expand Down Expand Up @@ -159,9 +169,44 @@ def decorator(func: Callable[Concatenate[Data, ...], R]) -> frame_constructor[R]

return decorator

def __call__(self, obj: Data, /, **kwds: Any) -> T_co:
"""Build a native frame from `obj` by delegating to the wrapped function."""
return self.func(obj, **kwds)
@overload
def __call__(
self: frame_constructor[IntoDataFrameT],
obj: Data,
/,
namespace: ModuleType,
**kwds: Any,
) -> DataFrame[IntoDataFrameT]: ...
@overload
def __call__(
self: frame_constructor[IntoLazyFrameT],
obj: Data,
/,
namespace: ModuleType,
**kwds: Any,
) -> LazyFrame[IntoLazyFrameT]: ...
@overload
def __call__(
self: frame_constructor[IntoFrame],
obj: Data,
/,
namespace: ModuleType,
**kwds: Any,
) -> DataFrame[Any] | LazyFrame[Any]: ...

def __call__(
self, obj: Data, /, namespace: ModuleType, **kwds: Any
) -> DataFrame[Any] | LazyFrame[Any]:
"""Build a native frame and wrap it with `namespace.from_native`.

Arguments:
obj: Column-oriented mapping passed to the wrapped builder.
namespace: A narwhals namespace (e.g. `narwhals`, `narwhals.stable.v1`)
whose `from_native` performs the wrapping.
**kwds: Forwarded to the wrapped builder.
"""
native = self.func(obj, **kwds)
return namespace.from_native(native) # type: ignore[no-any-return]

@property
def identifier(self) -> str:
Expand Down
73 changes: 56 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from importlib.util import find_spec
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

import pytest

Expand All @@ -15,16 +15,15 @@

if TYPE_CHECKING:
from collections.abc import Sequence

from typing_extensions import TypeAlias
from types import ModuleType

from narwhals._typing import EagerAllowed
from narwhals.testing.typing import DataFrameConstructor, FrameConstructor
from narwhals.typing import NonNestedDType
from narwhals.dataframe import DataFrame, LazyFrame
from narwhals.testing.constructors import frame_constructor
from narwhals.testing.typing import Data, DataFrameConstructor, FrameConstructor
from narwhals.typing import IntoFrame, NonNestedDType
from tests.utils import NestedOrEnumDType

Data: TypeAlias = "dict[str, list[Any]]"


# Narwhals-internal pytest options (not part of the public testing plugin)

Expand Down Expand Up @@ -120,22 +119,62 @@ def nested_dtype(request: pytest.FixtureRequest) -> NestedOrEnumDType:
return dtype


# The following fixtures are aliases of those registered in `narwhals/testing/pytest_plugin.py`
# in order to be backward compatible with the old fixture names and avoid having to change
# every single test.
# TODO(FBruzzesi): Rm once all tests start using nw_frame_constructor directly
# The following fixtures are aliases of those registered in `narwhals/testing/pytest_plugin.py`,
# wrapped so that calling them without an explicit `namespace` defaults to the main
# `narwhals` namespace. Tests can still pass `nw_v1` / `nw_v2` explicitly to opt in
# to a stable namespace; the legacy pattern `nw.from_native(constructor(data))` keeps
# working because `nw.from_native` is idempotent on narwhals objects.
# TODO(FBruzzesi): Drop these aliases once every test calls `nw_frame` / `nw_dataframe`
# directly with an explicit namespace.


class _PatchedFrameConstructor:
"""Proxy over a `frame_constructor` defaulting `namespace` to `narwhals`.

Delegates attribute access, `str()`, and `repr()` to the wrapped instance
so that test helpers (e.g. `constructor.is_nullable`, `"pandas" in str(constructor)`)
keep working unchanged.
"""

__slots__ = ("_inner",)

def __init__(self, inner: frame_constructor[IntoFrame]) -> None:
self._inner = inner

def __call__(
self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any
) -> DataFrame[Any] | LazyFrame[Any]:
return self._inner(obj, namespace=namespace, **kwds)

def __getattr__(self, name: str) -> Any:
return getattr(self._inner, name)

def __str__(self) -> str:
return str(self._inner)

def __repr__(self) -> str:
return repr(self._inner)


class _PatchedDataFrameConstructor(_PatchedFrameConstructor):
def __call__(
self, obj: Data, /, namespace: ModuleType = nw, **kwds: Any
) -> DataFrame[Any]:
return cast("DataFrame[Any]", self._inner(obj, namespace=namespace, **kwds))


@pytest.fixture
def constructor(nw_frame: FrameConstructor) -> FrameConstructor:
return nw_frame
def constructor(nw_frame: FrameConstructor) -> _PatchedFrameConstructor:
return _PatchedFrameConstructor(nw_frame)


@pytest.fixture
def constructor_eager(nw_dataframe: DataFrameConstructor) -> FrameConstructor:
return nw_dataframe
def constructor_eager(nw_dataframe: DataFrameConstructor) -> _PatchedDataFrameConstructor:
return _PatchedDataFrameConstructor(nw_dataframe)


@pytest.fixture
def constructor_pandas_like(
nw_pandas_like_frame: DataFrameConstructor,
) -> FrameConstructor:
return nw_pandas_like_frame
) -> _PatchedDataFrameConstructor:
return _PatchedDataFrameConstructor(nw_pandas_like_frame)
5 changes: 2 additions & 3 deletions tests/dependencies/is_narwhals_dataframe_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing import TYPE_CHECKING

import narwhals as nw
from narwhals.stable.v1.dependencies import is_narwhals_dataframe

if TYPE_CHECKING:
Expand All @@ -12,5 +11,5 @@
def test_is_narwhals_dataframe(constructor_eager: ConstructorEager) -> None:
df = constructor_eager({"col1": [1, 2], "col2": [3, 4]})

assert is_narwhals_dataframe(nw.from_native(df))
assert not is_narwhals_dataframe(df)
assert is_narwhals_dataframe(df)
assert not is_narwhals_dataframe(df.to_native())
5 changes: 2 additions & 3 deletions tests/dependencies/is_narwhals_lazyframe_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing import TYPE_CHECKING

import narwhals as nw
from narwhals.stable.v1.dependencies import is_narwhals_lazyframe
from tests.utils import Constructor

Expand All @@ -13,5 +12,5 @@
def test_is_narwhals_lazyframe(constructor: Constructor) -> None:
lf = constructor({"a": [1, 2, 3]})

assert is_narwhals_lazyframe(nw.from_native(lf).lazy())
assert not is_narwhals_lazyframe(lf)
assert is_narwhals_lazyframe(lf.lazy())
assert not is_narwhals_lazyframe(lf.to_native())
26 changes: 11 additions & 15 deletions tests/expr_and_series/arithmetic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_arithmetic_expr(
request.applymarker(pytest.mark.xfail)

data = {"a": [1.0, 2.0, 3.0]}
df = nw.from_native(constructor(data))
df = constructor(data, nw)
result = df.select(getattr(nw.col("a"), attr)(rhs))
assert_equal_data(result, {"a": expected})

Expand Down Expand Up @@ -76,7 +76,7 @@ def test_right_arithmetic_expr(
):
request.applymarker(pytest.mark.xfail)
data = {"a": [1, 2, 3]}
df = nw.from_native(constructor(data))
df = constructor(data)
result = df.select(getattr(nw.col("a"), attr)(rhs))
assert_equal_data(result, {"literal": expected})

Expand Down Expand Up @@ -107,7 +107,7 @@ def test_arithmetic_series(
request.applymarker(pytest.mark.xfail)

data = {"a": [1, 2, 3]}
df = nw.from_native(nw_dataframe(data), eager_only=True)
df = nw_dataframe(data, nw)
result = df.select(getattr(df["a"], attr)(rhs))
assert_equal_data(result, {"a": expected})

Expand Down Expand Up @@ -137,7 +137,7 @@ def test_right_arithmetic_series(
request.applymarker(pytest.mark.xfail)

data = {"a": [1, 2, 3]}
df = nw.from_native(nw_dataframe(data), eager_only=True)
df = nw_dataframe(data, nw)
result_series = getattr(df["a"], attr)(rhs)
assert result_series.name == "a"
assert_equal_data({"a": result_series}, {"a": expected})
Expand All @@ -149,8 +149,8 @@ def test_truediv_same_dims(
if "polars" in str(nw_dataframe):
# https://github.com/pola-rs/polars/issues/17760
request.applymarker(pytest.mark.xfail)
s_left = nw.from_native(nw_dataframe({"a": [1, 2, 3]}), eager_only=True)["a"]
s_right = nw.from_native(nw_dataframe({"a": [2, 2, 1]}), eager_only=True)["a"]
s_left = nw_dataframe({"a": [1, 2, 3]}, nw)["a"]
s_right = nw_dataframe({"a": [2, 2, 1]}, nw)["a"]
result = s_left / s_right
assert_equal_data({"a": result}, {"a": [0.5, 1.0, 3.0]})
result = s_left.__rtruediv__(s_right)
Expand All @@ -166,9 +166,7 @@ def test_floordiv(nw_dataframe: ConstructorEager, *, left: int, right: int) -> N
pytest.skip()
assume(right != 0)
expected = {"a": [left // right]}
result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select(
nw.col("a") // right
)
result = nw_dataframe({"a": [left]}, nw).select(nw.col("a") // right)
assert_equal_data(result, expected)


Expand All @@ -182,9 +180,7 @@ def test_mod(nw_dataframe: ConstructorEager, *, left: int, right: int) -> None:
pytest.skip()
assume(right != 0)
expected = {"a": [left % right]}
result = nw.from_native(nw_dataframe({"a": [left]}), eager_only=True).select(
nw.col("a") % right
)
result = nw_dataframe({"a": [left]}, nw).select(nw.col("a") % right)
assert_equal_data(result, expected)


Expand Down Expand Up @@ -218,7 +214,7 @@ def test_arithmetic_expr_left_literal(
request.applymarker(pytest.mark.xfail)

data = {"a": [1.0, 2.0, 4.0]}
df = nw.from_native(constructor(data))
df = constructor(data, nw)
result = df.select(getattr(lhs, attr)(nw.col("a")))
assert_equal_data(result, {"literal": expected})

Expand Down Expand Up @@ -249,7 +245,7 @@ def test_arithmetic_series_left_literal(
request.applymarker(pytest.mark.xfail)

data = {"a": [1.0, 2.0, 4.0]}
df = nw.from_native(nw_dataframe(data))
df = nw_dataframe(data, nw)
result = df.select(getattr(lhs, attr)(nw.col("a")))
assert_equal_data(result, {"literal": expected})

Expand All @@ -258,7 +254,7 @@ def test_std_broadcating(constructor: Constructor) -> None:
if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3):
# `std(ddof=2)` fails for duckdb here
pytest.skip()
df = nw.from_native(constructor({"a": [1, 2, 3]}))
df = constructor({"a": [1, 2, 3]}, nw)
result = df.with_columns(b=nw.col("a").std()).sort("a")
expected = {"a": [1, 2, 3], "b": [1.0, 1.0, 1.0]}
assert_equal_data(result, expected)
Expand Down
4 changes: 2 additions & 2 deletions tests/expr_and_series/dt/datetime_attributes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ def test_to_date(request: pytest.FixtureRequest, constructor: Constructor) -> No
request.applymarker(pytest.mark.xfail)
dates = {"a": [datetime(2001, 1, 1), None, datetime(2001, 1, 3)]}
if "dask" in str(constructor):
df_dask = cast("dd.DataFrame", constructor(dates))
df_dask = cast("dd.DataFrame", constructor(dates).to_native())
df_dask = cast("dd.DataFrame", df_dask.astype({"a": "timestamp[ns][pyarrow]"}))
df = nw.from_native(df_dask)
else:
df = nw.from_native(constructor(dates))
df = constructor(dates)
result = df.select(nw.col("a").dt.date())
assert result.collect_schema() == {"a": nw.Date}
2 changes: 1 addition & 1 deletion tests/expr_and_series/dt/datetime_duration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def test_duration_attributes_nano(
import numpy as np

data = {"c": np.array([None, 20], dtype="timedelta64[ns]")}
df = nw.from_native(constructor(data))
df = constructor(data, nw)

result_c = df.select(getattr(nw.col("c").dt, attribute)().fill_null(0))
assert_equal_data(result_c, {"c": expected_c})
Expand Down
2 changes: 1 addition & 1 deletion tests/expr_and_series/over_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def test_over_quantile(constructor: Constructor, request: pytest.FixtureRequest)
data = {"a": [1, 2, 3, 4, 5, 6], "b": ["x", "x", "x", "y", "y", "y"]}

quantile_expr = nw.col("a").quantile(quantile=0.5, interpolation="linear")
native_frame = constructor(data)
native_frame = constructor(data).to_native()

if "dask" in str(constructor):
native_frame = native_frame.repartition(npartitions=1) # type: ignore[union-attr]
Expand Down
Loading
Loading