Skip to content
12 changes: 8 additions & 4 deletions docs/api-reference/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ The plugin auto-loads as soon as you `pip install narwhals`. Just write a test:
```python
from typing import TYPE_CHECKING

import narwhals as nw
import narwhals.stable.v2 as nw_v2

if TYPE_CHECKING:
Expand All @@ -55,16 +54,19 @@ if TYPE_CHECKING:

def test_shape(nw_dataframe: DataFrameConstructor) -> None:
data: Data = {"x": [1, 2, 3]}
df = nw_dataframe(data, namespace=nw)
df = nw_dataframe(data) # (1)!
assert df.shape == (3, 1)


def test_laziness(nw_lazyframe: LazyFrameConstructor) -> None:
data: Data = {"x": [1, 2, 3]}
lf = nw_lazyframe(data, namespace=nw_v2)
lf = nw_lazyframe(data, namespace=nw_v2) # (2)!
assert isinstance(lf, nw_v2.LazyFrame)
```

1. Wraps with the main `narwhals` namespace by default
2. Opt in to a stable namespace

The fixtures are parametrised against every supported backend that is installed
in the current environment. Filter the matrix on the command line:

Expand All @@ -73,7 +75,7 @@ pytest --nw-backends="pandas,polars[lazy]"
pytest --all-nw-backends
```

## Type aliases
## Typing

::: narwhals.testing.typing
handler: python
Expand All @@ -82,7 +84,9 @@ pytest --all-nw-backends
heading_level: 3
members:
- Data
- ConstructorProtocol
- FrameConstructor
- DataFrameConstructor
- LazyFrameConstructor
- PandasConstructor
- NarwhalsNamespace
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ exclude_also = [
'if "(cudf|modin|pyspark|ibis)" in',
"if not .*.is_pandas",
"if is_ibis_table",
"if is_native_ibis",
"if MYPY",
"except ModuleNotFoundError",
'request.applymarker\(pytest.mark.xfail',
Expand Down
2 changes: 1 addition & 1 deletion src/narwhals/_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def from_native_object(
impl = Implementation.CUDF
elif is_native_modin(native): # pragma: no cover
impl = Implementation.MODIN
elif is_native_ibis(native): # pragma: no cover
elif is_native_ibis(native):
impl = Implementation.IBIS
else:
msg = f"Unsupported type: {type(native).__qualname__!r}"
Expand Down
44 changes: 15 additions & 29 deletions src/narwhals/testing/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame:
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, cast, overload

from narwhals._utils import Implementation, generate_temporary_column_name
from narwhals.testing.typing import ConstructorProtocol

if TYPE_CHECKING:
from collections.abc import Callable, Iterable
Expand Down Expand Up @@ -77,7 +78,9 @@ def my_backend_lazy_constructor(obj: Data, /, **kwds: Any) -> IntoLazyFrame:
R = TypeVar("R", bound="IntoFrame")


class frame_constructor(Generic[T_co]): # noqa: N801
class frame_constructor( # noqa: N801
ConstructorProtocol["DataFrame[Any] | LazyFrame[Any]"], Generic[T_co]
):
"""Callable wrapper around a backend frame builder.

Turns a column-oriented `dict` (typed as [`Data`][narwhals.testing.typing.Data])
Expand All @@ -86,6 +89,9 @@ class frame_constructor(Generic[T_co]): # noqa: N801
`func`. Equality and hashing are keyed on `(type, name)`, so two lookups
of the same registered constructor compare equal.

Implements [`ConstructorProtocol`][narwhals.testing.typing.ConstructorProtocol],
from which the member docstrings are inherited.

Warning:
Instances should be created via [`narwhals.testing.constructors.frame_constructor.register`][],
which is the only supported entry point.
Expand Down Expand Up @@ -171,119 +177,99 @@ def __call__(
self: frame_constructor[IntoDataFrameT],
obj: Data,
/,
namespace: NarwhalsNamespace,
namespace: NarwhalsNamespace | None = ...,
**kwds: Any,
) -> DataFrame[IntoDataFrameT]: ...
@overload
def __call__(
self: frame_constructor[IntoLazyFrameT],
obj: Data,
/,
namespace: NarwhalsNamespace,
namespace: NarwhalsNamespace | None = ...,
**kwds: Any,
) -> LazyFrame[IntoLazyFrameT]: ...
@overload
def __call__(
self: frame_constructor[IntoFrame],
obj: Data,
/,
namespace: NarwhalsNamespace,
namespace: NarwhalsNamespace | None = ...,
**kwds: Any,
) -> DataFrame[Any] | LazyFrame[Any]: ...
) -> DataFrame[IntoDataFrame] | LazyFrame[IntoLazyFrame]: ...

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

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.
"""
namespace = narwhals
native = self.func(obj, **kwds)
return namespace.from_native(native) # type: ignore[no-any-return]

@property
def identifier(self) -> str:
"""Instance-level string identifier for test IDs."""
return self.name

@property
def is_lazy(self) -> bool:
"""Whether this constructor produces a lazy native frame."""
return not self.is_eager

@property
def is_pandas(self) -> bool:
"""Whether this is one of the pandas constructors."""
return self.implementation.is_pandas()

@property
def is_modin(self) -> bool:
"""Whether this is one of the modin constructors."""
return self.implementation.is_modin()

@property
def is_cudf(self) -> bool:
"""Whether this is the cudf constructor."""
return self.implementation.is_cudf()

@property
def is_pandas_like(self) -> bool:
"""Whether this constructor produces a pandas-like dataframe (pandas, modin, cudf)."""
return self.implementation.is_pandas_like()

@property
def is_polars(self) -> bool:
"""Whether this is one of the polars constructors."""
return self.implementation.is_polars()

@property
def is_pyarrow(self) -> bool:
"""Whether this is the pyarrow table constructor."""
return self.implementation.is_pyarrow()

@property
def is_dask(self) -> bool:
"""Whether this is the dask constructor."""
return self.implementation.is_dask()

@property
def is_duckdb(self) -> bool:
"""Whether this is the duckdb constructor."""
return self.implementation.is_duckdb()

@property
def is_pyspark(self) -> bool:
"""Whether this is one of the pyspark constructors."""
impl = self.implementation
return impl.is_pyspark() or impl.is_pyspark_connect()

@property
def is_sqlframe(self) -> bool:
"""Whether this is the sqlframe constructor."""
return self.implementation.is_sqlframe()

@property
def is_ibis(self) -> bool:
"""Whether this is the ibis constructor."""
return self.implementation.is_ibis()

@property
def is_spark_like(self) -> bool:
"""Whether this constructor uses a spark-like backend (pyspark, sqlframe)."""
return self.implementation.is_spark_like()

@property
def needs_pyarrow(self) -> bool:
"""Whether this constructor requires `pyarrow` to be installed."""
return "pyarrow" in self.requirements

@property
def is_available(self) -> bool:
"""Whether every package this constructor needs is importable."""
return is_backend_available(*self.requirements)

def __str__(self) -> str:
Expand Down
8 changes: 5 additions & 3 deletions src/narwhals/testing/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Callable, Sequence

import pytest

Expand Down Expand Up @@ -87,7 +87,9 @@ def pytest_addoption(parser: pytest.Parser) -> None:
)


def _select_backends(config: pytest.Config) -> list[FrameConstructor]: # pragma: no cover
def _select_backends( # pragma: no cover
config: pytest.Config,
) -> Sequence[FrameConstructor]:
from narwhals.testing.constructors import (
available_default_cpu_backends,
prepare_backends,
Expand Down Expand Up @@ -128,4 +130,4 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
fixture_name = next(iter(matched_fixtures))
filter_fn = fixture_filters[fixture_name]
params = [c for c in selected if filter_fn(c)]
metafunc.parametrize(fixture_name, params, ids=[c.name for c in params])
metafunc.parametrize(fixture_name, params, ids=[c.identifier for c in params])
Loading
Loading